Browse Source

Merge pull request 'add separate screen for amount confirmations' (#15) from separate-dbs into master

Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/kick-starter-for-time-pwa/pulls/15
kb/add-usage-guide
anomalist 2 years ago
parent
commit
1d47a90836
  1. 15
      README.md
  2. 13
      project.yaml
  3. 2
      src/constants/app.ts
  4. 3
      src/db/tables/accounts.ts
  5. 3
      src/db/tables/settings.ts
  6. 40
      src/libs/endorserServer.ts
  7. 8
      src/main.ts
  8. 34
      src/router/index.ts
  9. 12
      src/test/index.ts
  10. 115
      src/views/AccountViewView.vue
  11. 3
      src/views/CommitmentsView.vue
  12. 384
      src/views/ContactAmountsView.vue
  13. 286
      src/views/ContactsView.vue
  14. 27
      src/views/ImportAccountView.vue
  15. 6
      src/views/NewEditAccountView.vue
  16. 26
      src/views/NewEditProjectView.vue
  17. 18
      src/views/ProjectViewView.vue
  18. 14
      src/views/ProjectsView.vue

15
README.md

@ -22,7 +22,8 @@ npm run lint
### Clear data & restart ### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start (because it'll regenerate if you start on the `/account` page). Clear cache for localhost, then go to http://localhost:8080/start
(because it'll generate a new one automatically if you start on the `/account` page).
### Test key contents ### Test key contents
@ -30,7 +31,8 @@ See [this page](openssl_signing_console.rst)
### Register new user on test server ### Register new user on test server
New users require registration. This can be done with a claim payload like this by an existing user: New users require registration. This can be done with a claim payload like this
by an existing user:
``` ```
const vcClaim = { const vcClaim = {
@ -42,18 +44,23 @@ New users require registration. This can be done with a claim payload like this
}; };
``` ```
On the test server, User #0 has rights to register others, so you can start playing one of two ways: On the test server, User #0 has rights to register others, so you can start
playing one of two ways:
- Import the keys for the test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` by importing this seed phrase: - Import the keys for the test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` by importing this seed phrase:
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control` `seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
- Alternatively, register someone else under User #0 on the `/account` page: - Alternatively, register someone else under User #0 automatically:
* In the `src/views/AccountViewView.vue` file, uncomment the lines referring to "testServerRegisterUser". * In the `src/views/AccountViewView.vue` file, uncomment the lines referring to "testServerRegisterUser".
* Visit the `/account` page. * Visit the `/account` page.
### Create multiple identifiers
Go to /import-account and import a new one. Then switch identifiers on the
bottom of the Your Identity page.
### Create keys with alternate tools ### Create keys with alternate tools

13
project.yaml

@ -12,15 +12,16 @@
- replace user-affecting console.logs with error messages (eg. catches) - replace user-affecting console.logs with error messages (eg. catches)
- contacts v1 : - contacts v1 :
- test confirmed vs unconfirmed amounts - .1 change "confirmed" flag to "amountConfirmed" on gives
- remove 'copy' until it works - .2 warn about amounts when you cannot see them
- switch to prod server - .1 remove 'copy' until it works
- 01 show gives with confirmations - .5 switch to prod server
- .5 Add page to show seed. - .5 Add page to show seed.
- 01 Provide a way to import the non-sensitive data. - 01 Provide a way to import the non-sensitive data.
- 01 Provide way to share your contact info. - 01 Provide way to share your contact info.
- .2 move all "identity" references to temporary account access - .2 move all "identity" references to temporary account access
- get 'copy' to work on account page - .5 make deploy for give-only features
- .5 get 'copy' to work on account page
- contacts v+ : - contacts v+ :
- .5 make advanced "show/hide amounts" button into a nice UI toggle - .5 make advanced "show/hide amounts" button into a nice UI toggle
@ -29,8 +30,10 @@
- refactor UI : - refactor UI :
- .5 Alerts show at the top and can be missed, eg. account data download - .5 Alerts show at the top and can be missed, eg. account data download
- 01 Change alerts into a component (to cut down duplicate code)
- 01 Code for "nav" tabs across the bottom is duplicated on each page. - 01 Code for "nav" tabs across the bottom is duplicated on each page.
- .2 Add "copied" feedback when they click "copy" on /account - .2 Add "copied" feedback when they click "copy" on /account
- .5 Fix how icons show on top of bottom bar on ContactAmounts page
- commit screen - commit screen

2
src/constants/app.ts

@ -2,7 +2,7 @@
* Generic strings that could be used throughout the app. * Generic strings that could be used throughout the app.
*/ */
export enum AppString { export enum AppString {
APP_NAME = "Kickstart for time", APP_NAME = "KickStart with Time",
VERSION = "0.1", VERSION = "0.1",
DEFAULT_ENDORSER_API_SERVER = "https://test.endorser.ch:8000", DEFAULT_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
//DEFAULT_ENDORSER_API_SERVER = "http://localhost:3000", //DEFAULT_ENDORSER_API_SERVER = "http://localhost:3000",

3
src/db/tables/accounts.ts

@ -2,6 +2,7 @@ export type Account = {
id?: number; // auto-generated by Dexie id?: number; // auto-generated by Dexie
dateCreated: string; dateCreated: string;
derivationPath: string; derivationPath: string;
did: string;
identity: string; identity: string;
publicKeyHex: string; publicKeyHex: string;
mnemonic: string; mnemonic: string;
@ -11,5 +12,5 @@ export type Account = {
// see https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon // see https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon
export const AccountsSchema = { export const AccountsSchema = {
accounts: accounts:
"++id, dateCreated, derivationPath, $identity, $mnemonic, publicKeyHex", "++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",
}; };

3
src/db/tables/settings.ts

@ -1,6 +1,7 @@
// a singleton // a singleton
export type Settings = { export type Settings = {
id: number; id: number; // there's only one entry: MASTER_SETTINGS_KEY
activeDid?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
showContactGivesInline?: boolean; showContactGivesInline?: boolean;

40
src/libs/endorserServer.ts

@ -0,0 +1,40 @@
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
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 GiveServerRecord {
agentDid: string;
amount: number;
amountConfirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
handleId: string;
issuedAt: string;
recipientDid: string;
unit: string;
}
export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": string;
agent: { identifier: string };
description?: string;
identifier?: string;
object: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string };
}
export interface RegisterVerifiableCredential {
"@context": string;
"@type": string;
agent: { identifier: string };
object: string;
recipient: { identifier: string };
}

8
src/main.ts

@ -12,6 +12,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { import {
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
faCircle,
faCircleCheck, faCircleCheck,
faCircleQuestion, faCircleQuestion,
faCircleUser, faCircleUser,
@ -19,9 +20,12 @@ import {
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFileLines,
faFolderOpen, faFolderOpen,
faHand, faHand,
faHouseChimney, faHouseChimney,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass, faMagnifyingGlass,
faPen, faPen,
faPersonCircleCheck, faPersonCircleCheck,
@ -40,6 +44,7 @@ import {
library.add( library.add(
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
faCircle,
faCircleCheck, faCircleCheck,
faCircleQuestion, faCircleQuestion,
faCircleUser, faCircleUser,
@ -47,9 +52,12 @@ library.add(
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFileLines,
faFolderOpen, faFolderOpen,
faHand, faHand,
faHouseChimney, faHouseChimney,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass, faMagnifyingGlass,
faPen, faPen,
faPersonCircleCheck, faPersonCircleCheck,

34
src/router/index.ts

@ -23,12 +23,6 @@ const routes: Array<RouteRecordRaw> = [
component: () => component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"), import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
}, },
{
path: "/start",
name: "start",
component: () =>
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
},
{ {
path: "/account", path: "/account",
name: "account", name: "account",
@ -43,6 +37,14 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "confirm-contact" */ "../views/ConfirmContactView.vue" /* webpackChunkName: "confirm-contact" */ "../views/ConfirmContactView.vue"
), ),
}, },
{
path: "/contact-amounts",
name: "contact-amounts",
component: () =>
import(
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
),
},
{ {
path: "/contacts", path: "/contacts",
name: "contacts", name: "contacts",
@ -93,12 +95,6 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-edit-commitment" */ "../views/NewEditCommitmentView.vue" /* webpackChunkName: "new-edit-commitment" */ "../views/NewEditCommitmentView.vue"
), ),
}, },
{
path: "/project",
name: "project",
component: () =>
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
},
{ {
path: "/new-edit-project", path: "/new-edit-project",
name: "new-edit-project", name: "new-edit-project",
@ -107,6 +103,12 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue" /* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
), ),
}, },
{
path: "/project",
name: "project",
component: () =>
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
},
{ {
path: "/projects", path: "/projects",
name: "projects", name: "projects",
@ -114,12 +116,10 @@ const routes: Array<RouteRecordRaw> = [
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"), import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
}, },
{ {
path: "/commitments", path: "/start",
name: "commitments", name: "start",
component: () => component: () =>
import( import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
/* webpackChunkName: "commitments" */ "../views/CommitmentsView.vue"
),
}, },
]; ];

12
src/test/index.ts

@ -1,9 +1,10 @@
import axios from "axios"; import axios from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { accountsDB } from "../db"; import { db } from "../db";
import { SERVICE_ID } from "../libs/veramo/setup"; import { SERVICE_ID } from "../libs/veramo/setup";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
export async function testServerRegisterUser() { export async function testServerRegisterUser() {
const testUser0Mnem = const testUser0Mnem =
@ -13,9 +14,8 @@ export async function testServerRegisterUser() {
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
await accountsDB.open(); await db.open();
const accounts = await accountsDB.accounts.toArray(); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const thisIdentity = JSON.parse(accounts[0].identity);
// Make a claim // Make a claim
const vcClaim = { const vcClaim = {
@ -23,7 +23,7 @@ export async function testServerRegisterUser() {
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { did: identity0.did }, agent: { did: identity0.did },
object: SERVICE_ID, object: SERVICE_ID,
participant: { did: thisIdentity.did }, participant: { did: settings?.activeDid },
}; };
// Make a payload for the claim // Make a payload for the claim
const vcPayload = { const vcPayload = {
@ -56,5 +56,5 @@ export async function testServerRegisterUser() {
}; };
const resp = await axios.post(url, payload, { headers }); const resp = await axios.post(url, payload, { headers });
console.log("Result:", resp); console.log("User registration result:", resp);
} }

115
src/views/AccountViewView.vue

@ -91,8 +91,8 @@
class="text-sm text-slate-500 flex justify-between items-center mb-1" class="text-sm text-slate-500 flex justify-between items-center mb-1"
> >
<span <span
><code>{{ address }}</code> ><code>{{ activeDid }}</code>
<button @click="copy(address)"> <button @click="copy(activeDid)">
<fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa> <fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa>
</button> </button>
</span> </span>
@ -202,11 +202,8 @@
</button> </button>
</div> </div>
<div class="flex"> <div class="flex py-2">
<button <button class="text-center text-md text-blue-500" @click="checkLimits()">
class="text-center text-md text-blue-500 px-1.5 py-2"
@click="checkLimits()"
>
Check Limits Check Limits
</button> </button>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9"> <div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
@ -225,6 +222,15 @@
</div> </div>
</div> </div>
<div v-if="numAccounts > 0" class="flex py-2">
Switch Account
<span v-for="accountNum in numAccounts" :key="accountNum">
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
#{{ accountNum }}
</button>
</span>
</div>
<div v-bind:class="computedAlertClassNames()"> <div v-bind:class="computedAlertClassNames()">
<button <button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2" class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@ -240,8 +246,11 @@
<script lang="ts"> <script lang="ts">
import "dexie-export-import"; import "dexie-export-import";
import * as R from "ramda";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
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 {
@ -250,7 +259,7 @@ import {
generateSeed, generateSeed,
newIdentifier, newIdentifier,
} from "@/libs/crypto"; } from "@/libs/crypto";
import { AppString } from "@/constants/app"; import { AxiosError } from "axios/index";
//import { testServerRegisterUser } from "../test"; //import { testServerRegisterUser } from "../test";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
@ -269,11 +278,11 @@ interface RateLimits {
components: {}, components: {},
}) })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
address = ""; activeDid = "";
derivationPath = ""; derivationPath = "";
firstName = ""; firstName = "";
lastName = ""; lastName = "";
mnemonic = ""; numAccounts = 0;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
limits: RateLimits | null = null; limits: RateLimits | null = null;
@ -296,22 +305,22 @@ export default class AccountViewView extends Vue {
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);
if (settings) { this.activeDid = settings?.activeDid || "";
this.firstName = settings.firstName || ""; this.firstName = settings?.firstName || "";
this.lastName = settings.lastName || ""; this.lastName = settings?.lastName || "";
this.showContactGives = !!settings.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
}
await accountsDB.open(); await accountsDB.open();
const numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
if (numAccounts === 0) { if (this.numAccounts === 0) {
let address = ""; // 0x... ETH address, without "did:eth:"
let privateHex = ""; let privateHex = "";
this.mnemonic = generateSeed(); const mnemonic = generateSeed();
[this.address, privateHex, this.publicHex, this.derivationPath] = [address, privateHex, this.publicHex, this.derivationPath] =
deriveAddress(this.mnemonic); deriveAddress(mnemonic);
const newId = newIdentifier( const newId = newIdentifier(
this.address, address,
this.publicHex, this.publicHex,
privateHex, privateHex,
this.derivationPath this.derivationPath
@ -319,18 +328,24 @@ export default class AccountViewView extends Vue {
await accountsDB.accounts.add({ await accountsDB.accounts.add({
dateCreated: new Date().toISOString(), dateCreated: new Date().toISOString(),
derivationPath: this.derivationPath, derivationPath: this.derivationPath,
did: newId.did,
identity: JSON.stringify(newId), identity: JSON.stringify(newId),
mnemonic: this.mnemonic, mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex, publicKeyHex: newId.keys[0].publicKeyHex,
}); });
this.activeDid = newId.did;
} }
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === this.activeDid, accounts);
this.address = identity.did; const identity = JSON.parse(account?.identity || "undefined");
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;
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
} 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). See console log for more info.";
@ -344,12 +359,9 @@ export default class AccountViewView extends Vue {
this.showContactGives = !this.showContactGives; this.showContactGives = !this.showContactGives;
try { try {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); db.settings.update(MASTER_SETTINGS_KEY, {
if (settings) { showContactGivesInline: this.showContactGives,
db.settings.update(MASTER_SETTINGS_KEY, { });
showContactGivesInline: this.showContactGives,
});
}
} 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). See console log for more info.";
@ -387,7 +399,8 @@ export default class AccountViewView extends Vue {
const url = endorserApiServer + "/api/report/rateLimits"; const url = endorserApiServer + "/api/report/rateLimits";
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -396,25 +409,43 @@ export default class AccountViewView extends Vue {
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
// axios throws an exception on a 400
if (resp.status === 200) { if (resp.status === 200) {
this.limits = resp.data; this.limits = resp.data;
} else {
this.alertTitle = "Error from Server";
console.log("Bad response retrieving limits: ", resp.data);
if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message;
} else {
this.alertMessage = "Bad server response of " + resp.status;
}
this.isAlertVisible = true;
} }
} catch (err) { } catch (error: unknown) {
const serverError = error as AxiosError;
this.alertTitle = "Error from Server"; this.alertTitle = "Error from Server";
this.alertMessage = err as string; console.log("Bad response retrieving limits: ", serverError);
// Anybody know how to access items inside "response.data" without this?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = serverError.response?.data;
if (data.error.message) {
this.alertMessage = data.error.message;
} else {
this.alertMessage = "Bad server response. See logs for details.";
}
this.isAlertVisible = true; this.isAlertVisible = true;
} }
} }
async switchAccount(accountNum: number) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = accounts[accountNum - 1];
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
this.activeDid = account.did;
this.derivationPath = account.derivationPath;
this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
}
public showContactGivesClassNames() { public showContactGivesClassNames() {
return { return {
"bg-slate-900": !this.showContactGives, "bg-slate-900": !this.showContactGives,

3
src/views/CommitmentsView.vue

@ -1,3 +0,0 @@
<template>
<section id="Content" class="p-6 pb-24"></section>
</template>

384
src/views/ContactAmountsView.vue

@ -0,0 +1,384 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200">
<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>
<!-- Contacts -->
<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>
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Given with {{ contact?.name }}
</h1>
<!-- Results List -->
<div>
<div class="border-b border-slate-300 flex">
<div class="w-1/4"></div>
<div class="w-1/4">from them</div>
<div class="w-1/4"></div>
<div class="w-1/4">to them</div>
</div>
<div
class="border-b border-slate-300 flex"
v-for="record in giveRecords"
:key="record.id"
>
<div class="w-1/4">
{{ new Date(record.issuedAt).toLocaleString() }}
</div>
<div class="w-1/4">
<span v-if="record.agentDid == contact.did">
<div class="font-bold">
{{ record.amount }} {{ record.unit }}
<span v-if="record.amountConfirmed" class="tooltip">
<fa icon="circle-check" class="text-green-600 fa-fw ml-1" />
<span class="tooltiptext">Confirmed</span>
</span>
<button v-else class="tooltip" @click="confirm(record)">
<fa icon="circle" class="text-blue-600 fa-fw ml-1" />
<span class="tooltiptext">Unconfirmed</span>
</button>
</div>
<br />
{{ record.description }}
</span>
</div>
<div class="w-1/8">
<span v-if="record.agentDid == contact.did">
<fa icon="long-arrow-alt-left" class="text-slate-900 fa-fw ml-1" />
</span>
<span v-else>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<fa icon="long-arrow-alt-right" class="text-slate-900 fa-fw ml-1" />
</span>
</div>
<div class="w-1/4">
<span v-if="record.agentDid != contact.did">
<div class="font-bold">
{{ record.amount }} {{ record.unit }}
<span v-if="record.amountConfirmed" class="tooltip">
<fa icon="circle-check" class="text-green-600 fa-fw ml-1" />
<span class="tooltiptext">Confirmed</span>
</span>
<button v-else class="tooltip" @click="cannotConfirmMessage()">
<fa icon="circle" class="text-slate-600 fa-fw ml-1" />
<span class="tooltiptext">Unconfirmed</span>
</button>
</div>
<br />
{{ record.description }}
</span>
</div>
</div>
</div>
<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>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { AppString } from "@/constants/app";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import {
AgreeVerifiableCredential,
GiveServerRecord, GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT,
} from "@/libs/endorserServer";
import * as didJwt from "did-jwt";
import { AxiosError } from "axios";
@Options({})
export default class ContactsView extends Vue {
activeDid = "";
contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = [];
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const contactDid = this.$route.query.contactDid as string;
this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact);
}
}
async loadGives(activeDid: string, contact: Contact) {
// only load the private keys temporarily when needed
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// load all the time I have given to them
try {
let result = [];
const url =
endorserApiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did) +
"&recipientDid=" +
encodeURIComponent(contact.did);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
result = resp.data.data;
} else {
console.log(
"Got bad response status & data of",
resp.status,
resp.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
const url2 =
endorserApiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(contact.did) +
"&recipientDid=" +
encodeURIComponent(identity.did);
const token2 = await accessToken(identity);
const headers2 = {
"Content-Type": "application/json",
Authorization: "Bearer " + token2,
};
const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) {
result = R.concat(result, resp2.data.data);
} else {
console.log(
"Got bad response status & data of",
resp2.status,
resp2.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
const sortedResult: Array<GiveServerRecord> = R.sort(
(a, b) =>
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
result
);
this.giveRecords = sortedResult;
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
}
async confirm(record: GiveServerRecord) {
// Make claim
// I use clone here because otherwise it gets a Proxy object.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const origClaim: GiveVerifiableCredential =
R.clone(record.fullClaim);
if (record.fullClaim["@context"] == SCHEMA_ORG_CONTEXT) {
delete origClaim["@context"];
}
origClaim["identifier"] = record.handleId;
const vcClaim: AgreeVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "AgreeAction",
object: origClaim,
};
// 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
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
if (identity.keys[0].privateKeyHex !== null) {
// 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 endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success) {
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
// Now set that error for the user to see.
this.alertTitle = "Error With Server";
this.alertMessage = userMessage;
this.isAlertVisible = true;
}
}
}
cannotConfirmMessage() {
this.alertTitle = "Not Allowed";
this.alertMessage = "Only the recipient can confirm final receipt.";
this.isAlertVisible = true;
}
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
absolute: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>
<style>
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
}
.tooltip:hover .tooltiptext-left {
visibility: visible;
}
</style>

286
src/views/ContactsView.vue

@ -91,7 +91,7 @@
> >
{{ {{
showGiveTotals showGiveTotals
? "Totals" ? "Total"
: showGiveConfirmed : showGiveConfirmed
? "Confirmed" ? "Confirmed"
: "Unconfirmed" : "Unconfirmed"
@ -122,10 +122,10 @@
@click="setVisibility(contact, false)" @click="setVisibility(contact, false)"
> >
<fa icon="eye" class="text-slate-900 fa-fw ml-1" /> <fa icon="eye" class="text-slate-900 fa-fw ml-1" />
<span class="tooltiptext">Can see you</span> <span class="tooltiptext">They can see you</span>
</button> </button>
<button v-else class="tooltip" @click="setVisibility(contact, true)"> <button v-else class="tooltip" @click="setVisibility(contact, true)">
<span class="tooltiptext">Cannot see you</span> <span class="tooltiptext">They cannot see you</span>
<fa icon="eye-slash" class="text-slate-900 fa-fw ml-1" /> <fa icon="eye-slash" class="text-slate-900 fa-fw ml-1" />
</button> </button>
@ -139,7 +139,7 @@
<fa icon="person-circle-check" class="text-slate-900 fa-fw ml-1" /> <fa icon="person-circle-check" class="text-slate-900 fa-fw ml-1" />
</button> </button>
<button v-else @click="register(contact)" class="tooltip"> <button v-else @click="register(contact)" class="tooltip">
<span class="tooltiptext">Maybe not registered</span> <span class="tooltiptext">Registration Unknown</span>
<fa <fa
icon="person-circle-question" icon="person-circle-question"
class="text-slate-900 fa-fw ml-1" class="text-slate-900 fa-fw ml-1"
@ -165,18 +165,18 @@
: (givenByMeUnconfirmed[contact.did] || 0) : (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
<span class="tooltiptext-left">{{ <span class="tooltiptext-left">
givenByMeDescriptions[contact.did] {{ givenByMeDescriptions[contact.did] || "Nothing" }}
}}</span> </span>
<button <button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="onClickAddGive(identity.did, contact.did)" @click="onClickAddGive(activeDid, contact.did)"
> >
+ +
</button> </button>
</div> </div>
<div class="tooltip px-2"> <div class="tooltip px-2">
by: from:
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
this.showGiveTotals this.showGiveTotals
@ -188,21 +188,32 @@
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
<span class="tooltiptext-left"> <span class="tooltiptext-left">
{{ givenToMeDescriptions[contact.did] }} {{ givenToMeDescriptions[contact.did] || "Nothing" }}
</span> </span>
<button <button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="onClickAddGive(contact.did, identity.did)" @click="onClickAddGive(contact.did, activeDid)"
> >
+ +
</button> </button>
</div> </div>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="tooltip"
>
<fa icon="file-lines" class="text-slate-600 fa-fw ml-1" />
<span class="tooltiptext-left">See All Given Activity</span>
</router-link>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
</ul> </ul>
</section> </section>
<div v-bind:class="computedAlertClassNames()"> <div v-bind:class="computedAlertClassNames()">
<button <button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2" class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@ -223,48 +234,25 @@ import { IIdentifier } from "@veramo/core";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import {
GiveServerRecord,
GiveVerifiableCredential,
RegisterVerifiableCredential,
SERVICE_ID,
} from "@/libs/endorserServer";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
const SERVICE_ID = "endorser.ch";
export interface GiveServerRecord {
agentDid: string;
amount: number;
confirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
handleId: string;
recipientDid: string;
unit: string;
}
export interface GiveVerifiableCredential {
"@context": string;
"@type": string;
agent: { identifier: string };
description?: string;
object: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string };
}
export interface RegisterVerifiableCredential {
"@context": string;
"@type": string;
agent: { identifier: string };
object: string;
recipient: { identifier: string };
}
@Options({ @Options({
components: {}, components: {},
}) })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
activeDid = "";
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
contactInput = ""; contactInput = "";
// { "did:...": concatenated-descriptions } entry for each contact // { "did:...": concatenated-descriptions } entry for each contact
@ -281,19 +269,16 @@ export default class ContactsView extends Vue {
givenToMeUnconfirmed: Record<string, number> = {}; givenToMeUnconfirmed: Record<string, number> = {};
hourDescriptionInput = ""; hourDescriptionInput = "";
hourInput = "0"; hourInput = "0";
identity: IIdentifier | null = null;
showGiveNumbers = false; showGiveNumbers = false;
showGiveTotals = true; showGiveTotals = true;
showGiveConfirmed = true; showGiveConfirmed = true;
// 'created' hook runs when the Vue instance is first created // 'created' hook runs when the Vue instance is first created
async created() { async created() {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
this.identity = JSON.parse(accounts[0].identity);
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.showGiveNumbers = !!settings?.showContactGivesInline; this.showGiveNumbers = !!settings?.showContactGivesInline;
if (this.showGiveNumbers) { if (this.showGiveNumbers) {
this.loadGives(); this.loadGives();
@ -305,35 +290,13 @@ export default class ContactsView extends Vue {
); );
} }
async onClickNewContact(): Promise<void> {
let did = this.contactInput;
let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(",");
if (commaPos1 > -1) {
did = this.contactInput.substring(0, commaPos1).trim();
name = this.contactInput.substring(commaPos1 + 1).trim();
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
if (commaPos2 > -1) {
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim();
}
}
// help with potential mistakes while this sharing requires copy-and-paste
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
// it must be all hex (compressed public key), so convert
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
}
const newContact = { did, name, publicKeyBase64 };
await db.contacts.add(newContact);
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
);
}
async loadGives() { async loadGives() {
if (!this.identity) { await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
if (!identity) {
console.error( console.error(
"Attempted to load Give records with no identity available." "Attempted to load Give records with no identity available."
); );
@ -346,15 +309,15 @@ export default class ContactsView extends Vue {
try { try {
const url = const url =
endorserApiServer + endorserApiServer +
"/api/v2/report/gives?agentId=" + "/api/v2/report/gives?agentDid=" +
encodeURIComponent(this.identity?.did); encodeURIComponent(identity.did);
const token = await accessToken(this.identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
console.log("All your gifts:", resp.data); console.log("All gifts you've given:", resp.data);
if (resp.status === 200) { if (resp.status === 200) {
const contactDescriptions: Record<string, string> = {}; const contactDescriptions: Record<string, string> = {};
const contactConfirmed: Record<string, number> = {}; const contactConfirmed: Record<string, number> = {};
@ -363,24 +326,36 @@ export default class ContactsView extends Vue {
for (const give of allData) { for (const give of allData) {
if (give.unit == "HUR") { if (give.unit == "HUR") {
const recipDid: string = give.recipientDid; const recipDid: string = give.recipientDid;
if (give.confirmed) { if (give.amountConfirmed) {
const prevAmount = contactConfirmed[recipDid] || 0; const prevAmount = contactConfirmed[recipDid] || 0;
contactConfirmed[recipDid] = prevAmount + give.amount; contactConfirmed[recipDid] = prevAmount + give.amount;
} else { } else {
const prevAmount = contactUnconfirmed[recipDid] || 0; const prevAmount = contactUnconfirmed[recipDid] || 0;
contactUnconfirmed[recipDid] = prevAmount + give.amount; contactUnconfirmed[recipDid] = prevAmount + give.amount;
} }
const prevDesc = contactDescriptions[recipDid] || ""; if (!contactDescriptions[recipDid] && give.description) {
// Since many make the tooltip too big, we'll just use the latest; // Since many make the tooltip too big, we'll just use the latest.
contactDescriptions[recipDid] = give.description || prevDesc; contactDescriptions[recipDid] = give.description;
}
} }
} }
//console.log("Done retrieving gives", contactConfirmed); //console.log("Done retrieving gives", contactConfirmed);
this.givenByMeDescriptions = contactDescriptions; this.givenByMeDescriptions = contactDescriptions;
this.givenByMeConfirmed = contactConfirmed; this.givenByMeConfirmed = contactConfirmed;
this.givenByMeUnconfirmed = contactUnconfirmed;
} else {
console.log(
"Got bad response status & data of",
resp.status,
resp.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
} }
} catch (error) { } catch (error) {
this.alertTitle = "Error from Server"; this.alertTitle = "Error With Server";
this.alertMessage = error as string; this.alertMessage = error as string;
this.isAlertVisible = true; this.isAlertVisible = true;
} }
@ -389,9 +364,9 @@ export default class ContactsView extends Vue {
try { try {
const url = const url =
endorserApiServer + endorserApiServer +
"/api/v2/report/gives?recipientId=" + "/api/v2/report/gives?recipientDid=" +
encodeURIComponent(this.identity.did); encodeURIComponent(identity.did);
const token = await accessToken(this.identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
@ -405,29 +380,68 @@ export default class ContactsView extends Vue {
const allData: Array<GiveServerRecord> = resp.data.data; const allData: Array<GiveServerRecord> = resp.data.data;
for (const give of allData) { for (const give of allData) {
if (give.unit == "HUR") { if (give.unit == "HUR") {
if (give.confirmed) { if (give.amountConfirmed) {
const prevAmount = contactConfirmed[give.agentDid] || 0; const prevAmount = contactConfirmed[give.agentDid] || 0;
contactConfirmed[give.agentDid] = prevAmount + give.amount; contactConfirmed[give.agentDid] = prevAmount + give.amount;
} else { } else {
const prevAmount = contactUnconfirmed[give.agentDid] || 0; const prevAmount = contactUnconfirmed[give.agentDid] || 0;
contactUnconfirmed[give.agentDid] = prevAmount + give.amount; contactUnconfirmed[give.agentDid] = prevAmount + give.amount;
} }
const prevDesc = contactDescriptions[give.agentDid] || ""; if (!contactDescriptions[give.agentDid] && give.description) {
// Since many make the tooltip too big, we'll just use the latest; // Since many make the tooltip too big, we'll just use the latest.
contactDescriptions[give.agentDid] = give.description || prevDesc; contactDescriptions[give.agentDid] = give.description;
}
} }
} }
//console.log("Done retrieving receipts", contactConfirmed); //console.log("Done retrieving receipts", contactConfirmed);
this.givenToMeDescriptions = contactDescriptions; this.givenToMeDescriptions = contactDescriptions;
this.givenToMeConfirmed = contactConfirmed; this.givenToMeConfirmed = contactConfirmed;
this.givenToMeUnconfirmed = contactUnconfirmed;
} else {
console.log(
"Got bad response status & data of",
resp.status,
resp.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your received time from the server.";
this.isAlertVisible = true;
} }
} catch (error) { } catch (error) {
this.alertTitle = "Error from Server"; this.alertTitle = "Error With Server";
this.alertMessage = error as string; this.alertMessage = error as string;
this.isAlertVisible = true; this.isAlertVisible = true;
} }
} }
async onClickNewContact(): Promise<void> {
let did = this.contactInput;
let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(",");
if (commaPos1 > -1) {
did = this.contactInput.substring(0, commaPos1).trim();
name = this.contactInput.substring(commaPos1 + 1).trim();
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
if (commaPos2 > -1) {
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim();
}
}
// help with potential mistakes while this sharing requires copy-and-paste
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
// it must be all hex (compressed public key), so convert
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
}
const newContact = { did, name, publicKeyBase64 };
await db.contacts.add(newContact);
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
);
}
async deleteContact(contact: Contact) { async deleteContact(contact: Contact) {
if ( if (
confirm( confirm(
@ -454,7 +468,9 @@ 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 identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
// Make a claim // Make a claim
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
@ -518,7 +534,7 @@ export default class ContactsView extends Vue {
userMessage = error as string; userMessage = error as string;
} }
// Now set that error for the user to see. // Now set that error for the user to see.
this.alertTitle = "Error with Server"; this.alertTitle = "Error With Server";
this.alertMessage = userMessage; this.alertMessage = userMessage;
this.isAlertVisible = true; this.isAlertVisible = true;
} }
@ -534,7 +550,9 @@ export default class ContactsView extends Vue {
(visibility ? "canSeeMe" : "cannotSeeMe"); (visibility ? "canSeeMe" : "cannotSeeMe");
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -548,7 +566,7 @@ export default class ContactsView extends Vue {
contact.seesMe = visibility; contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility }); db.contacts.update(contact.did, { seesMe: visibility });
} else { } else {
this.alertTitle = "Error from Server"; this.alertTitle = "Error With Server";
console.log("Bad response setting visibility: ", resp.data); console.log("Bad response setting visibility: ", resp.data);
if (resp.data.error?.message) { if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message; this.alertMessage = resp.data.error?.message;
@ -558,7 +576,7 @@ export default class ContactsView extends Vue {
this.isAlertVisible = true; this.isAlertVisible = true;
} }
} catch (err) { } catch (err) {
this.alertTitle = "Error from Server"; this.alertTitle = "Error With Server";
this.alertMessage = err as string; this.alertMessage = err as string;
this.isAlertVisible = true; this.isAlertVisible = true;
} }
@ -572,7 +590,9 @@ export default class ContactsView extends Vue {
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -594,7 +614,7 @@ export default class ContactsView extends Vue {
"see your activity."; "see your activity.";
this.isAlertVisible = true; this.isAlertVisible = true;
} else { } else {
this.alertTitle = "Error from Server"; this.alertTitle = "Error With Server";
if (resp.data.error?.message) { if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message; this.alertMessage = resp.data.error?.message;
} else { } else {
@ -603,7 +623,7 @@ export default class ContactsView extends Vue {
this.isAlertVisible = true; this.isAlertVisible = true;
} }
} catch (err) { } catch (err) {
this.alertTitle = "Error from Server"; this.alertTitle = "Error With Server";
this.alertMessage = err as string; this.alertMessage = err as string;
this.isAlertVisible = true; this.isAlertVisible = true;
} }
@ -625,6 +645,27 @@ export default class ContactsView extends Vue {
} }
async onClickAddGive(fromDid: string, toDid: string): Promise<void> { async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
// if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
if (
confirm(
"There are " +
this.givenToMeUnconfirmed[fromDid] +
" unconfirmed hours from them." +
" Would you like to confirm some of those hours?"
)
) {
this.$router.push({
name: "contact-amounts",
query: { contactDid: fromDid },
});
}
}
if (!this.isNumeric(this.hourInput)) { if (!this.isNumeric(this.hourInput)) {
this.alertTitle = "Input Error"; this.alertTitle = "Input Error";
this.alertMessage = this.alertMessage =
@ -634,13 +675,14 @@ export default class ContactsView extends Vue {
this.alertTitle = "Input Error"; this.alertTitle = "Input Error";
this.alertMessage = "Giving 0 hours does nothing."; this.alertMessage = "Giving 0 hours does nothing.";
this.isAlertVisible = true; this.isAlertVisible = true;
} else if (!this.identity) { } else if (!identity) {
this.alertTitle = "Status Error"; this.alertTitle = "Status Error";
this.alertMessage = "No identity is available."; this.alertMessage = "No identity is available.";
this.isAlertVisible = true; this.isAlertVisible = true;
} else { } else {
// ask to confirm amount
let toFrom; let toFrom;
if (fromDid == this.identity?.did) { if (fromDid == identity?.did) {
toFrom = "from you to " + this.nameForDid(this.contacts, toDid); toFrom = "from you to " + this.nameForDid(this.contacts, toDid);
} else { } else {
toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you"; toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you";
@ -662,7 +704,7 @@ export default class ContactsView extends Vue {
) )
) { ) {
this.createAndSubmitGive( this.createAndSubmitGive(
this.identity, identity,
fromDid, fromDid,
toDid, toDid,
parseFloat(this.hourInput), parseFloat(this.hourInput),
@ -726,20 +768,17 @@ export default class ContactsView extends Vue {
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?.handleId) {
this.alertTitle = ""; this.alertTitle = "Done";
this.alertMessage = ""; this.alertMessage = "Successfully logged time to the server.";
this.isAlertVisible = true;
if (fromDid === identity.did) { if (fromDid === identity.did) {
this.givenByMeConfirmed[toDid] = const newList = R.clone(this.givenByMeUnconfirmed);
this.givenByMeConfirmed[toDid] + amount; newList[toDid] = (newList[toDid] || 0) + amount;
// do this to update the UI (is there a better way?) this.givenByMeUnconfirmed = newList;
// eslint-disable-next-line no-self-assign
this.givenByMeConfirmed = this.givenByMeConfirmed;
} else { } else {
this.givenToMeConfirmed[fromDid] = const newList = R.clone(this.givenToMeConfirmed);
this.givenToMeConfirmed[fromDid] + amount; newList[fromDid] = (newList[fromDid] || 0) + amount;
// do this to update the UI (is there a better way?) this.givenToMeConfirmed = newList;
// eslint-disable-next-line no-self-assign
this.givenToMeConfirmed = this.givenToMeConfirmed;
} }
} }
} catch (error) { } catch (error) {
@ -755,26 +794,13 @@ export default class ContactsView extends Vue {
userMessage = error as string; userMessage = error as string;
} }
// Now set that error for the user to see. // Now set that error for the user to see.
this.alertTitle = "Error with Server"; this.alertTitle = "Error With Server";
this.alertMessage = userMessage; this.alertMessage = userMessage;
this.isAlertVisible = true; this.isAlertVisible = true;
} }
} }
} }
public selectedGiveTotal(
contactGivesConfirmed: Record<string, number>,
contactGivesUnconfirmed: Record<string, number>,
did: string
) {
/* eslint-disable prettier/prettier */
this.showGiveTotals
? ((contactGivesConfirmed[did] || 0) + (contactGivesUnconfirmed[did] || 0))
: this.showGiveConfirmed
? (contactGivesConfirmed[did] || 0)
: (contactGivesUnconfirmed[did] || 0);
/* eslint-enable prettier/prettier */
}
public toggleShowGiveTotals() { public toggleShowGiveTotals() {
if (this.showGiveTotals) { if (this.showGiveTotals) {
this.showGiveTotals = false; this.showGiveTotals = false;
@ -817,7 +843,7 @@ export default class ContactsView extends Vue {
public showGiveAmountsClassNames() { public showGiveAmountsClassNames() {
return { return {
"bg-slate-900": this.showGiveTotals, "bg-slate-500": this.showGiveTotals,
"bg-green-600": !this.showGiveTotals && this.showGiveConfirmed, "bg-green-600": !this.showGiveTotals && this.showGiveConfirmed,
"bg-yellow-600": !this.showGiveTotals && !this.showGiveConfirmed, "bg-yellow-600": !this.showGiveTotals && !this.showGiveConfirmed,
}; };

27
src/views/ImportAccountView.vue

@ -45,7 +45,8 @@
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { accountsDB } from "@/db"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Options({ @Options({
components: {}, components: {},
@ -76,16 +77,20 @@ export default class ImportAccountView extends Vue {
try { try {
await accountsDB.open(); await accountsDB.open();
const num_accounts = await accountsDB.accounts.count(); await accountsDB.accounts.add({
if (num_accounts === 0) { dateCreated: new Date().toISOString(),
await accountsDB.accounts.add({ derivationPath: this.derivationPath,
dateCreated: new Date().toISOString(), did: newId.did,
derivationPath: this.derivationPath, identity: JSON.stringify(newId),
identity: JSON.stringify(newId), mnemonic: mne,
mnemonic: mne, publicKeyHex: newId.keys[0].publicKeyHex,
publicKeyHex: newId.keys[0].publicKeyHex, });
});
} // record that as the active DID
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
this.$router.push({ name: "account" }); this.$router.push({ name: "account" });
} catch (err) { } catch (err) {
console.log("Error!"); console.log("Error!");

6
src/views/NewEditAccountView.vue

@ -70,10 +70,8 @@ export default class NewEditAccountView extends Vue {
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
if (settings) { this.firstName = settings?.firstName || "";
this.firstName = settings.firstName || ""; this.lastName = settings?.lastName || "";
this.lastName = settings.lastName || "";
}
} }
onClickSaveChanges() { onClickSaveChanges() {

26
src/views/NewEditProjectView.vue

@ -76,14 +76,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { accountsDB } from "../db"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { IIdentifier } from "@veramo/core";
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
import { AxiosError } from "axios"; import { IIdentifier } from "@veramo/core";
interface VerifiableCredential { interface VerifiableCredential {
"@context": string; "@context": string;
@ -97,6 +100,7 @@ interface VerifiableCredential {
components: {}, components: {},
}) })
export default class NewEditProjectView extends Vue { export default class NewEditProjectView extends Vue {
activeDid = "";
projectName = ""; projectName = "";
description = ""; description = "";
errorMessage = ""; errorMessage = "";
@ -109,16 +113,19 @@ export default class NewEditProjectView 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() {
if (this.projectId === "") { await db.open();
console.log("This is a new project"); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
} else { this.activeDid = settings?.activeDid || "";
if (this.projectId) {
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.log("Problem! Should have a profile!"); console.log("Problem! Should have a profile!");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProject(identity); this.LoadProject(identity);
} }
} }
@ -257,7 +264,8 @@ export default class NewEditProjectView extends Vue {
console.log("Problem! Should have a profile!"); console.log("Problem! Should have a profile!");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.SaveProject(identity); this.SaveProject(identity);
} }
} }

18
src/views/ProjectViewView.vue

@ -146,13 +146,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios";
import * as moment from "moment";
import * as R from "ramda";
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { accountsDB } from "../db";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { AppString } from "@/constants/app";
import * as moment from "moment";
import { AxiosError } from "axios";
@Options({ @Options({
components: {}, components: {},
@ -226,13 +229,18 @@ export default class ProjectViewView 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() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
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.log("Problem! Should have a profile!"); console.log("Problem! Should have a profile!");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProject(identity); this.LoadProject(identity);
} }
} }

14
src/views/ProjectsView.vue

@ -101,11 +101,14 @@
</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 { AppString } from "@/constants/app";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { accountsDB } from "../db";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { AppString } from "@/constants/app";
@Options({ @Options({
components: {}, components: {},
@ -155,13 +158,18 @@ export default class ProjectsView 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() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
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.log("Problem! Should have a profile!"); console.log("Problem! Should have a profile!");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProjects(identity); this.LoadProjects(identity);
} }
} }

Loading…
Cancel
Save