Compare commits

..

9 Commits

Author SHA1 Message Date
Matthew Aaron Raymer
cf4375164f Adding router-links 2022-11-30 13:09:12 +08:00
Jose Olarte III
bdb9da2b87 Added back fontawesome icons 2022-11-28 21:04:26 +08:00
Matthew Aaron Raymer
6fc070b45d Chimney added to project view 2022-11-28 19:03:23 +08:00
Matthew Aaron Raymer
55ba4f0154 Updates with Font-Awesome 2022-11-28 18:31:38 +08:00
Matthew Aaron Raymer
38d04566a4 Merge branch 'tmp' of ssh://173.199.124.46:222/trent_larson/kick-starter-for-time-pwa into tmp 2022-11-28 17:52:19 +08:00
Matthew Aaron Raymer
4b8466fb04 First font-awesome icon 2022-11-28 17:50:51 +08:00
Jose Olarte III
f45a528f43 Fixes to input fields
Padding and border defaults not implicitly defaulted
2022-11-28 17:42:25 +08:00
Matthew Aaron Raymer
d25f8f45bd All the router endpoints to views initiated 2022-11-28 13:11:29 +08:00
Matthew Aaron Raymer
fee0c08f76 Review universal nav from boilerplate 2022-11-28 12:40:17 +08:00
38 changed files with 1101 additions and 18684 deletions

View File

@@ -15,6 +15,5 @@ module.exports = {
rules: { rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off",
}, },
}; };

161
README.md
View File

@@ -20,166 +20,5 @@ npm run build
npm run lint npm run lint
``` ```
### Clear data & restart
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
See [this page](openssl_signing_console.rst)
### Register new user on test server
New users require registration. This can be done with a claim payload like this
by an existing user:
```
const vcClaim = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { identifier: identity0.did },
object: SERVICE_ID,
participant: { identifier: newIdentity.did },
};
```
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:
`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 automatically:
* In the `src/views/AccountViewView.vue` file, uncomment the lines referring to "testServerRegisterUser".
* 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
See [this page](openssl_signing_console.rst)
### Customize configuration ### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/). See [Configuration Reference](https://cli.vuejs.org/config/).
## Dependencies
See https://tea.xyz
| Project | Version |
| ---------- | --------- |
| nodejs.org | ^16.0.0 |
| npmjs.com | ^8.0.0 |
## Other
```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
// Import an existing ID
export const importAndStoreIdentifier = async (mnemonic: string, mnemonicPassword: string, toLowercase: boolean, previousIdentifiers: Array<IIdentifier>) => {
// just to get rid of variability that might cause an error
mnemonic = mnemonic.trim().toLowerCase()
/**
// an approach I pieced together
// requires: yarn add elliptic
// ... plus:
// const EC = require('elliptic').ec
// const secp256k1 = new EC('secp256k1')
//
const keyHex: string = bip39.mnemonicToEntropy(mnemonic)
// returns a KeyPair from the elliptic.ec library
const keyPair = secp256k1.keyFromPrivate(keyHex, 'hex')
// this code is from did-provider-eth createIdentifier
const privateHex = keyPair.getPrivate('hex')
const publicHex = keyPair.getPublic('hex')
const address = didJwt.toEthereumAddress(publicHex)
**/
/**
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
// ... which almost works but the didJwt.toEthereumAddress is wrong
// requires: yarn add bip32
// ... plus: import * as bip32 from 'bip32'
//
const seed: Buffer = await bip39.mnemonicToSeed(mnemonic)
const root = bip32.fromSeed(seed)
const node = root.derivePath(UPORT_ROOT_DERIVATION_PATH)
const privateHex = node.privateKey.toString("hex")
const publicHex = node.publicKey.toString("hex")
const address = didJwt.toEthereumAddress('0x' + publicHex)
**/
/**
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
// requires: yarn add @ethersproject/hdnode
// ... plus: import { HDNode } from '@ethersproject/hdnode'
**/
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic)
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH)
const privateHex = rootNode.privateKey.substring(2) // original starts with '0x'
const publicHex = rootNode.publicKey.substring(2) // original starts with '0x'
let address = rootNode.address
const prevIds = previousIdentifiers || [];
if (toLowercase) {
const foundEqual = R.find(
(id) => utility.rawAddressOfDid(id.did) === address,
prevIds
)
if (foundEqual) {
// They're trying to create a lowercase version of one that exists in normal case.
// (We really should notify the user.)
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a normal-case version of the DID since a regular version exists."}))
} else {
address = address.toLowerCase()
}
} else {
// They're not trying to convert to lowercase.
const foundLower = R.find((id) =>
utility.rawAddressOfDid(id.did) === address.toLowerCase(),
prevIds
)
if (foundLower) {
// They're trying to create a normal case version of one that exists in lowercase.
// (We really should notify the user.)
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a lowercase version of the DID since a lowercase version exists."}))
address = address.toLowerCase()
}
}
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... derived keys and address..."}))
const newId = newIdentifier(address, publicHex, privateHex, UPORT_ROOT_DERIVATION_PATH)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... created new ID..."}))
// awaiting because otherwise the UI may not see that a mnemonic was created
const savedId = await storeIdentifier(newId, mnemonic, mnemonicPassword)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... stored new ID..."}))
return savedId
}
// Create a totally new ID
export const createAndStoreIdentifier = async (mnemonicPassword) => {
// This doesn't give us the entropy/seed.
//const id = await agent.didManagerCreate()
const entropy = crypto.randomBytes(32)
const mnemonic = bip39.entropyToMnemonic(entropy)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... generated mnemonic..."}))
return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, [])
}
```

View File

@@ -1,45 +0,0 @@
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
Generate an ECDSA key pair using the secp256k1 curve:
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
openssl ec -in private.pem -pubout -out public.pem
First, create a header object as a JSON object containing the alg (algorithm) and typ (type) fields. For example:
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
Next, create a payload object as a JSON object containing the claims you want to include in the JWT. For example schema.org :
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
Concatenate the encoded header, payload, and a secret to create the signing input:
signing_input="$header_b64.$payload_b64"
Create the signature by signing the signing input with a ES256K algorithm and your secret. You can use the openssl command line utility to do this:
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
Finally, encode the signature as a base64Url string and concatenate it with the signing input to create the JWT:
signature_b64=$(echo -n "$signature" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
jwt="$signing_input.$signature_b64"
This JWT can then be passed in the Authorization header of a HTTP request as a bearer token, for example:
Authorization: Bearer $jwt
To verify the JWT, you can use the openssl utility with the public key:
openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
This will verify the signature and output Verified OK if the signature is valid. If the signature is not valid, it will output an error.

15766
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,54 +8,19 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@ethersproject/hdnode": "^5.7.0", "@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/vue-fontawesome": "^3.0.2",
"@fortawesome/vue-fontawesome": "^3.0.3", "core-js": "^3.26.1",
"@pvermeer/dexie-encrypted-addon": "^2.0.5",
"@veramo/core": "^5.1.2",
"@veramo/credential-w3c": "^5.1.4",
"@veramo/data-store": "^5.1.2",
"@veramo/did-manager": "^5.1.2",
"@veramo/did-provider-ethr": "^5.1.2",
"@veramo/did-resolver": "^5.1.2",
"@veramo/key-manager": "^5.1.2",
"@vueuse/core": "^9.13.0",
"@zxing/text-encoding": "^0.9.0",
"axios": "^1.3.4",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1",
"core-js": "^3.29.1",
"dexie": "^3.2.3",
"dexie-export-import": "^4.0.7",
"did-jwt": "^6.11.5",
"ethereum-cryptography": "^1.2.0",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0",
"js-generate-password": "^0.1.7",
"localstorage-slim": "^2.4.0",
"luxon": "^3.3.0",
"merkletreejs": "^0.3.9",
"moment": "^2.29.4",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0",
"ramda": "^0.28.0",
"readable-stream": "^4.3.0",
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"vue": "^3.2.47", "vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-class-component": "^8.0.0-0", "vue-class-component": "^8.0.0-0",
"vue-facing-decorator": "^2.1.19",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"web-did-resolver": "^2.0.22" "vuex": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/ramda": "^0.28.23", "@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.44.0",
"@typescript-eslint/parser": "^5.57.0",
"@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8", "@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8", "@vue/cli-plugin-pwa": "~5.0.8",
@@ -64,14 +29,14 @@
"@vue/cli-plugin-vuex": "~5.0.8", "@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.2", "@vue/eslint-config-typescript": "^11.0.2",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.13",
"eslint": "^8.37.0", "eslint": "^8.28.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.10.0", "eslint-plugin-vue": "^9.8.0",
"postcss": "^8.4.21", "postcss": "^8.4.19",
"prettier": "^2.8.7", "prettier": "^2.8.0",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.2.4",
"typescript": "~5.0.3" "typescript": "~4.9.3"
} }
} }

View File

@@ -1,47 +0,0 @@
- top screens from img/screens.pdf milestone:2 :
- view all :
- add infinite scroll assignee:matthew
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- replace user-affecting console.logs with error messages (eg. catches)
- contacts v1 :
- .1 remove 'copy' until it works
- .5 switch to prod server
- .5 Add page to show seed.
- 01 Provide a way to import the non-sensitive data.
- 01 Provide way to share your contact info.
- .2 move all "identity" references to temporary account access
- .5 make deploy for give-only features
- .5 get 'copy' to work on account page
- contacts v+ :
- .5 make advanced "show/hide amounts" button into a nice UI toggle
- .2 show error to user when adding a duplicate contact
- parse input more robustly (with CSV lib and not commas)
- refactor UI :
- .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.
- .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
- discover screen
- backup all data
- Next Viable Product afterward
- Connect with phone contacts
- Multiple identities
- Peer DID
- DIDComm
- v video for multiple identities https://youtu.be/p8L87AeD76w
- v video for adding time to contacts https://youtu.be/7Yylczevp10

View File

@@ -3,5 +3,3 @@
</template> </template>
<style></style> <style></style>
<script lang="ts"></script>

View File

@@ -9,10 +9,3 @@
font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important; font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important;
} }
} }
@layer components {
input:checked ~ .dot {
transform: translateX(100%);
background-color: #FFF !important;
}
}

View File

@@ -1,42 +0,0 @@
<template>
<div ref="scrollContainer">
<slot />
<div ref="sentinel" style="height: 1px"></div>
</div>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
@Component
export default class InfiniteScroll extends Vue {
@Prop({ default: 100 })
readonly distance!: number;
private observer!: IntersectionObserver;
mounted() {
const options = {
root: this.$refs.scrollContainer as HTMLElement,
rootMargin: `0px 0px ${this.distance}px 0px`,
threshold: 1.0,
};
this.observer = new IntersectionObserver(this.handleIntersection, options);
this.observer.observe(this.$refs.sentinel as HTMLElement);
}
beforeUnmount() {
this.observer.disconnect();
}
@Emit("reached-bottom")
handleIntersection(entries: IntersectionObserverEntry[]) {
const entry = entries[0];
if (entry.isIntersecting) {
return true;
}
return false;
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

View File

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

View File

@@ -1,61 +0,0 @@
import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import { Account, AccountsSchema } from "./tables/accounts";
import { Contact, ContactsSchema } from "./tables/contacts";
import {
MASTER_SETTINGS_KEY,
Settings,
SettingsSchema,
} from "./tables/settings";
// a separate DB because the seed is super-sensitive data
type SensitiveTables = {
accounts: Table<Account>;
};
type NonsensitiveTables = {
contacts: Table<Contact>;
settings: Table<Settings>;
};
/**
* In order to make the next line be acceptable, the program needs to have its linter suppress a rule:
* https://typescript-eslint.io/rules/no-unnecessary-type-constraint/
*
* and change *any* to *unknown*
*
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
*/
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
export const accountsDB = new BaseDexie("KickStartAccounts") as SensitiveDexie;
const SensitiveSchemas = Object.assign({}, AccountsSchema);
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
BaseDexie & T;
export const db = new BaseDexie("KickStart") as NonsensitiveDexie;
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
/**
* Needed to enable a special webpack setting to allow *await* below:
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
*/
// create password and place password in localStorage
const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
if (localStorage.getItem("secret") == null) {
localStorage.setItem("secret", secret);
}
//console.log("IndexedDB Encryption Secret:", secret);
encrypted(accountsDB, { secretKey: secret });
accountsDB.version(1).stores(SensitiveSchemas);
db.version(1).stores(NonsensitiveSchemas);
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
db.on("populate", function () {
// ensure there's an initial entry for settings
db.settings.add({ id: MASTER_SETTINGS_KEY });
});

View File

@@ -1,16 +0,0 @@
export type Account = {
id?: number; // auto-generated by Dexie
dateCreated: string;
derivationPath: string;
did: string;
identity: string;
publicKeyHex: string;
mnemonic: string;
};
// mark encrypted field by starting with a $ character
// see https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon
export const AccountsSchema = {
accounts:
"++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",
};

View File

@@ -1,11 +0,0 @@
export interface Contact {
did: string;
name?: string;
publicKeyBase64?: string;
seesMe?: boolean;
registered?: boolean;
}
export const ContactsSchema = {
contacts: "++did, name, publicKeyBase64, registered, seesMe",
};

View File

@@ -1,14 +0,0 @@
// a singleton
export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY
activeDid?: string;
firstName?: string;
lastName?: string;
showContactGivesInline?: boolean;
};
export const SettingsSchema = {
settings: "id",
};
export const MASTER_SETTINGS_KEY = 1;

View File

@@ -1,150 +0,0 @@
import { IIdentifier } from "@veramo/core";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { getRandomBytesSync } from "ethereum-cryptography/random";
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays";
/**
*
*
* @param {string} address
* @param {string} publicHex
* @param {string} privateHex
* @param {string} derivationPath
* @return {*} {Omit<IIdentifier, 'provider'>}
*/
export const newIdentifier = (
address: string,
publicHex: string,
privateHex: string,
derivationPath: string
): Omit<IIdentifier, keyof "provider"> => {
return {
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
keys: [
{
kid: publicHex,
kms: "local",
meta: { derivationPath: derivationPath },
privateKeyHex: privateHex,
publicKeyHex: publicHex,
type: "Secp256k1",
},
],
provider: DEFAULT_DID_PROVIDER_NAME,
services: [],
};
};
/**
*
*
* @param {string} mnemonic
* @return {*} {[string, string, string, string]}
*/
export const deriveAddress = (
mnemonic: string
): [string, string, string, string] => {
const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
mnemonic = mnemonic.trim().toLowerCase();
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH);
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
const address = rootNode.address;
return [address, privateHex, publicHex, UPORT_ROOT_DERIVATION_PATH];
};
/**
*
*
* @return {*} {string}
*/
export const generateSeed = (): string => {
const entropy: Uint8Array = getRandomBytesSync(32);
const mnemonic = entropyToMnemonic(entropy, wordlist);
return mnemonic;
};
/**
* Retreive an access token
*
* @param {IIdentifier} identifier
* @return {*}
*/
export const accessToken = async (identifier: IIdentifier) => {
const did: string = identifier.did;
const privateKeyHex: string = identifier.keys[0].privateKeyHex as string;
const signer = SimpleSigner(privateKeyHex);
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt: string = await didJwt.createJWT(tokenPayload, {
alg,
issuer: did,
signer,
});
return jwt;
};
export const sign = async (privateKeyHex: string) => {
const signer = SimpleSigner(privateKeyHex);
return signer;
};
/**
* Copied out of did-jwt since it's deprecated in that library.
*
* The SimpleSigner returns a configured function for signing data.
*
* @example
* const signer = SimpleSigner(process.env.PRIVATE_KEY)
* signer(data, (err, signature) => {
* ...
* })
*
* @param {String} hexPrivateKey a hex encoded private key
* @return {Function} a configured signer function
*/
export function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
return async (data) => {
const signature = (await signer(data)) as string;
return fromJose(signature);
};
}
// from did-jwt/util; see SimpleSigner above
export function fromJose(signature: string): {
r: string;
s: string;
recoveryParam?: number;
} {
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`
);
}
const r = bytesToHex(signatureBytes.slice(0, 32));
const s = bytesToHex(signatureBytes.slice(32, 64));
const recoveryParam =
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
return { r, s, recoveryParam };
}
// from did-jwt/util; see SimpleSigner above
export function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}

View File

@@ -1,40 +0,0 @@
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 };
}

View File

@@ -1,100 +0,0 @@
/* import * as R from "ramda";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { IIdentifier } from "@veramo/core";
import { Contact } from "../entity/contact";
import { Settings } from "../entity/settings";
import * as utility from "../utility/utility";
const MAX_LOG_LENGTH = 2000000;
export const DEFAULT_ENDORSER_API_SERVER = "https://endorser.ch:3000";
export const DEFAULT_ENDORSER_VIEW_SERVER = "https://endorser.ch";
export const LOCAL_ENDORSER_API_SERVER = "http://127.0.0.1:3000";
export const LOCAL_ENDORSER_VIEW_SERVER = "http://127.0.0.1:3001";
export const TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000";
export const TEST_ENDORSER_VIEW_SERVER = "https://test.endorser.ch:8080";
// for contents set in reducers
interface Payload<T> {
type: string;
payload: T;
}
interface LogMsg {
log: boolean;
msg: string;
}
export const appSlice = createSlice({
name: "app",
initialState: {
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
settings: null as Settings,
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
identifiers: null as Array<IIdentifier> | null,
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
contacts: null as Array<Contact> | null,
viewServer: DEFAULT_ENDORSER_VIEW_SERVER,
logMessage: "",
advancedMode: false,
testMode: false,
},
reducers: {
addIdentifier: (state, contents: Payload<IIdentifier>) => {
state.identifiers = state.identifiers.concat([contents.payload]);
},
addLog: (state, contents: Payload<LogMsg>) => {
if (state.logMessage.length > MAX_LOG_LENGTH) {
state.logMessage =
"<truncated>\n..." +
state.logMessage.substring(
state.logMessage.length - MAX_LOG_LENGTH / 2
);
}
if (contents.payload.log) {
console.log(contents.payload.msg);
state.logMessage += "\n" + contents.payload.msg;
}
},
setAdvancedMode: (state, contents: Payload<boolean>) => {
state.advancedMode = contents.payload;
},
setContacts: (state, contents: Payload<Array<Contact>>) => {
state.contacts = contents.payload;
},
setContact: (state, contents: Payload<Contact>) => {
const index = R.findIndex(
(c) => c.did === contents.payload.did,
state.contacts
);
state.contacts[index] = contents.payload;
},
setHomeScreen: (state, contents: Payload<string>) => {
state.settings.homeScreen = contents.payload;
},
setIdentifiers: (state, contents: Payload<Array<IIdentifier>>) => {
state.identifiers = contents.payload;
},
setSettings: (state, contents: Payload<Settings>) => {
state.settings = contents.payload;
},
setTestMode: (state, contents: Payload<boolean>) => {
state.testMode = contents.payload;
},
setViewServer: (state, contents: Payload<string>) => {
state.viewServer = contents.payload;
},
},
});
export const appStore = configureStore({ reducer: appSlice.reducer });
*/

View File

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

View File

@@ -1,83 +1,54 @@
import { createPinia } from "pinia";
import { createApp } from "vue"; import { createApp } from "vue";
import App from "./App.vue"; import App from "./App.vue";
import "./registerServiceWorker"; import "./registerServiceWorker";
import router from "./router"; import router from "./router";
import axios from "axios"; import store from "./store";
import VueAxios from "vue-axios";
import "./assets/styles/tailwind.css"; import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core"; import { library } from "@fortawesome/fontawesome-svg-core";
import { import {
faCalendar,
faChevronLeft, faChevronLeft,
faCircle, faHouseChimney,
faCircleCheck, faMagnifyingGlass,
faCircleQuestion,
faCircleUser,
faCopy,
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFolderOpen, faFolderOpen,
faHand, faHand,
faHouseChimney, faCircleUser,
faLongArrowAltLeft, faCopy,
faLongArrowAltRight,
faMagnifyingGlass,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQrcode,
faRotate,
faShareNodes, faShareNodes,
faSpinner, faQrcode,
faTrashCan,
faUser, faUser,
faUsers, faPen,
faXmark, faTrashCan,
faCalendar,
faEllipsisVertical,
faSpinner,
faCircleCheck,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
faCalendar,
faChevronLeft, faChevronLeft,
faCircle, faHouseChimney,
faCircleCheck, faMagnifyingGlass,
faCircleQuestion,
faCircleUser,
faCopy,
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFolderOpen, faFolderOpen,
faHand, faHand,
faHouseChimney, faCircleUser,
faLongArrowAltLeft, faCopy,
faLongArrowAltRight,
faMagnifyingGlass,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQrcode,
faRotate,
faShareNodes, faShareNodes,
faSpinner, faQrcode,
faTrashCan,
faUser, faUser,
faUsers, faPen,
faXmark faTrashCan,
faCalendar,
faEllipsisVertical,
faSpinner,
faCircleCheck
); );
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
createApp(App) createApp(App)
.component("fa", FontAwesomeIcon) .component("fa", FontAwesomeIcon)
.use(createPinia()) .use(store)
.use(VueAxios, axios)
.use(router) .use(router)
.mount("#app"); .mount("#app");

View File

@@ -1,28 +1,27 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db"; import HomeView from "../views/HomeView.vue";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: "/", path: "/",
name: "home", name: "home",
component: () => component: HomeView,
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"),
beforeEnter: async (to, from, next) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
},
}, },
{ {
path: "/about", path: "/about",
name: "about", name: "about",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
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",
@@ -37,20 +36,6 @@ 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",
name: "contacts",
component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
},
{ {
path: "/scan-contact", path: "/scan-contact",
name: "scan-contact", name: "scan-contact",
@@ -65,12 +50,6 @@ const routes: Array<RouteRecordRaw> = [
component: () => component: () =>
import(/* webpackChunkName: "discover" */ "../views/DiscoverView.vue"), import(/* webpackChunkName: "discover" */ "../views/DiscoverView.vue"),
}, },
{
path: "/help",
name: "help",
component: () =>
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
},
{ {
path: "/import-account", path: "/import-account",
name: "import-account", name: "import-account",
@@ -109,21 +88,8 @@ const routes: Array<RouteRecordRaw> = [
component: () => component: () =>
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"), import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
}, },
{
path: "/projects",
name: "projects",
component: () =>
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
},
{
path: "/start",
name: "start",
component: () =>
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
},
]; ];
/** @type {*} */
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(process.env.BASE_URL),
routes, routes,

View File

@@ -1,20 +0,0 @@
// @ts-check
import { defineStore } from "pinia";
export const useAppStore = defineStore({
id: "app",
state: () => ({
_projectId:
typeof localStorage.getItem("projectId") === "undefined"
? ""
: localStorage.getItem("projectId"),
}),
getters: {
projectId: (state): string => state._projectId as string,
},
actions: {
async setProjectId(newProjectId: string) {
localStorage.setItem("projectId", newProjectId);
},
},
});

9
src/store/index.ts Normal file
View File

@@ -0,0 +1,9 @@
import { createStore } from "vuex";
export default createStore({
state: {},
getters: {},
mutations: {},
actions: {},
modules: {},
});

View File

@@ -1,60 +0,0 @@
import axios from "axios";
import * as didJwt from "did-jwt";
import { AppString } from "@/constants/app";
import { db } from "../db";
import { SERVICE_ID } from "../libs/veramo/setup";
import { deriveAddress, newIdentifier } from "../libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
export async function testServerRegisterUser() {
const testUser0Mnem =
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Make a claim
const vcClaim = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { did: identity0.did },
object: SERVICE_ID,
participant: { did: settings?.activeDid },
};
// Make a payload for the claim
const vcPayload = {
sub: "RegisterAction",
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// create a signature using private key of identity
// eslint-disable-next-line
const privateKeyHex: string = identity0.keys[0].privateKeyHex!;
const signer = await didJwt.SimpleSigner(privateKeyHex);
const alg = undefined;
// create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity0.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/claim";
const headers = {
"Content-Type": "application/json",
};
const resp = await axios.post(url, payload, { headers });
console.log("User registration result:", resp);
}

View File

@@ -1,48 +1,36 @@
<template> <template>
<!-- QUICK NAV --> <!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50"> <nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200">
<ul class="flex text-2xl p-2 gap-2"> <ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed --> <!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"> <a href="" class="block text-center py-3 px-1"
<fa icon="house-chimney" class="fa-fw"></fa> ><fa icon="house-chimney" class="fa-fw"></fa
</router-link> ></a>
</li> </li>
<!-- Search --> <!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="search.html" class="block text-center py-3 px-1"
:to="{ name: 'discover' }" ><fa icon="magnifying-glass" class="fa-fw"></fa
class="block text-center py-3 px-1" ></a>
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li> </li>
<!-- Projects --> <!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="" class="block text-center py-3 px-1"
:to="{ name: 'projects' }" ><fa icon="folder-open" class="fa-fw"></fa
class="block text-center py-3 px-1" ></a>
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li> </li>
<!-- Contacts --> <!-- Commitments -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="" class="block text-center py-3 px-1"
:to="{ name: 'contacts' }" ><fa icon="hand" class="fa-fw"></fa
class="block text-center py-3 px-1" ></a>
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li> </li>
<!-- Profile --> <!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white"> <li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link <a href="account-view.html" class="block text-center py-3 px-1"
:to="{ name: 'account' }" ><fa icon="circle-user" class="fa-fw"></fa
class="block text-center py-3 px-1" ></a>
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li> </li>
</ul> </ul>
</nav> </nav>
@@ -54,18 +42,6 @@
Your Identity Your Identity
</h1> </h1>
<div class="flex justify-between py-2">
<span />
<span>
<router-link
:to="{ name: 'help' }"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
>
Help
</router-link>
</span>
</div>
<!-- Friend referral requirement notice --> <!-- Friend referral requirement notice -->
<div <div
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"
@@ -84,21 +60,17 @@
<!-- Identity Details --> <!-- Identity Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-semibold mb-2">{{ firstName }} {{ lastName }}</h2> <h2 class="text-xl font-semibold mb-2">Firstname Lastname</h2>
<div class="text-slate-500 text-sm font-bold">ID</div> <div class="text-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1"> <div
<code class="truncate">{{ activeDid }}</code> class="text-sm text-slate-500 flex justify-between items-center mb-1"
<button >
@click=" <span
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy)) ><code>did:peer:kl45kj41lk451kl3</code>
" <fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa
class="ml-2" ></span>
> <span>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDidCopy">Copied!</span>
<span class="whitespace-nowrap ml-4">
<button <button
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md" class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
> >
@@ -112,71 +84,49 @@
</span> </span>
</div> </div>
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div> <div class="text-slate-500 text-sm font-bold">Public Key</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1"> <div class="text-sm text-slate-500 mb-1">
<code class="truncate">{{ publicBase64 }}</code> <span
<button ><code>dyIgKepL19trfrFu5jzkoNhI</code>
@click=" <fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy)) ></span>
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showB64Copy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ publicHex }}</code>
<button
@click="
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showPubCopy">Copied!</span>
</div> </div>
<div class="text-slate-500 text-sm font-bold">Derivation Path</div> <div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1"> <div class="text-sm text-slate-500 mb-1">
<code class="truncate">{{ derivationPath }}</code> <span
<button ><code>m/44'/0'/0'/0/0</code>
@click=" <fa icon="copy" class="text-slate-400 fa-fw ml-1"></fa
doCopyTwoSecRedo(derivationPath, () => (showDerCopy = !showDerCopy)) ></span>
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDerCopy">Copied!</span>
</div> </div>
</div> </div>
<router-link <a
:to="{ name: 'new-edit-account' }" href="account-edit.html"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8" class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
>Edit Identity</a
> >
Edit Identity
</router-link>
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3> <h3 class="text-sm uppercase font-semibold mb-3">Contact Actions</h3>
<a
href="contact-scan.html"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
>Scan New Contact</a
>
<h3 class="text-sm uppercase font-semibold mb-3">Identity Actions</h3>
<a <a
href="" href=""
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>Backup Seed</a
> >
Backup Identifier Seed
</a>
<a <a
href=""
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="exportDatabase()" >Backup Other Data</a
> >
Download Settings & Contacts (excluding Identifier Data)
</a>
<a ref="downloadLink" />
<!-- QR code popup --> <!-- QR code popup -->
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md"> <dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
@@ -200,322 +150,14 @@
</button> </button>
</form> </form>
</dialog> </dialog>
<h3 class="text-sm uppercase font-semibold mb-3">Advanced</h3>
<label
for="toggleShowAmounts"
class="flex items-center cursor-pointer mb-6"
@click="handleChange"
>
<!-- toggle -->
<div class="relative">
<!-- input -->
<input
type="checkbox"
v-model="showContactGives"
name="showContactGives"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
<!-- label -->
<div class="ml-2">Show amounts given with contacts</div>
</label>
<div class="flex py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits
</button>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<div 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()">
<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> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import "dexie-export-import"; import { Options, Vue } from "vue-class-component";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; @Options({
import { useClipboard } from "@vueuse/core"; components: {},
})
import { AppString } from "@/constants/app"; export default class AccountViewView extends Vue {}
import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
accessToken,
deriveAddress,
generateSeed,
newIdentifier,
} from "@/libs/crypto";
import { AxiosError } from "axios/index";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface RateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
@Component
export default class AccountViewView extends Vue {
activeDid = "";
derivationPath = "";
firstName = "";
lastName = "";
numAccounts = 0;
publicHex = "";
publicBase64 = "";
limits: RateLimits | null = null;
showContactGives = false;
showDidCopy = false;
showDerCopy = false;
showB64Copy = false;
showPubCopy = false;
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text, fn) {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
}
handleChange() {
this.showContactGives = !this.showContactGives;
this.updateShowContactAmounts();
}
readableTime(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T"));
}
// 'created' hook runs when the Vue instance is first created
async created() {
// Uncomment to register this user on the test server.
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
//testServerRegisterUser();
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.firstName = settings?.firstName || "";
this.lastName = settings?.lastName || "";
this.showContactGives = !!settings?.showContactGivesInline;
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 account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath;
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.log("Telling user to clear cache because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
}
}
public async updateShowContactAmounts() {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: this.showContactGives,
});
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.log("Telling user to clear cache because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
}
}
public async exportDatabase() {
try {
const blob = await db.export({ prettyJson: true });
const url = URL.createObjectURL(blob);
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.download = db.name + "-backup.json";
downloadAnchor.click();
URL.revokeObjectURL(url);
this.alertTitle = "Download Started";
this.alertMessage = "See your downloads directory for the backup.";
this.isAlertVisible = true;
} catch (error) {
this.alertTitle = "Export Error";
this.alertMessage = "See console logs for more info.";
this.isAlertVisible = true;
console.error("Export Error:", error);
}
}
async checkLimits() {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/report/rateLimits";
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");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
// axios throws an exception on a 400
if (resp.status === 200) {
this.limits = resp.data;
}
} catch (error: unknown) {
const serverError = error as AxiosError;
this.alertTitle = "Error from Server";
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;
}
}
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() {
return {
"bg-slate-900": !this.showContactGives,
"bg-green-600": this.showContactGives,
};
}
alertMessage = "";
alertTitle = "";
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,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script> </script>

View File

@@ -5,11 +5,11 @@
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel --> <!-- Cancel -->
<router-link <a
:to="{ name: 'account' }" href="account-view.html"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa ><fa icon="chevron-left" class="fa-fw"></fa
></router-link> ></a>
Confirm Contact Confirm Contact
</h1> </h1>

View File

@@ -1,384 +0,0 @@
<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>
<!-- 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,
fixed: 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>

View File

@@ -4,11 +4,11 @@
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel --> <!-- Cancel -->
<router-link <a
:to="{ name: 'account' }" href="account-view.html"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa ><fa icon="chevron-left" class="fa-fw"></fa
></router-link> ></a>
Scan Contact Scan Contact
</h1> </h1>

View File

@@ -1,919 +0,0 @@
<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>
<!-- 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 bg-slate-400 text-white">
<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">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Contacts
</h1>
<div class="flex justify-between py-2">
<span />
<span>
<router-link
:to="{ name: 'help' }"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
>
Help
</router-link>
</span>
</div>
<!-- New Contact -->
<div class="mb-4 flex">
<input
type="text"
placeholder="DID, Name, Public Key"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
v-model="contactInput"
/>
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
@click="onClickNewContact()"
>
<fa icon="plus" class="fa-fw"></fa>
</button>
</div>
<div class="flex justify-between" v-if="showGiveNumbers">
<div class="w-full text-right">
Hours to Add:
<input
class="border border rounded border-slate-400 w-24 text-right"
type="text"
placeholder="1"
v-model="hourInput"
/>
<br />
<input
class="border border rounded border-slate-400 w-48"
type="text"
placeholder="Description"
v-model="hourDescriptionInput"
/>
<br />
<br />
<button
href=""
class="text-center text-md text-white px-1.5 py-2 rounded-md mb-6"
v-bind:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()"
>
{{
showGiveTotals
? "Total"
: showGiveConfirmed
? "Confirmed"
: "Unconfirmed"
}}
</button>
</div>
</div>
<!-- Results List -->
<ul class="">
<li
class="border-b border-slate-300"
v-for="contact in contacts"
:key="contact.did"
>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">
{{ contact.name || "(no name)" }}
</h2>
<div class="text-sm truncate">{{ contact.did }}</div>
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
Public Key (base 64): {{ contact.publicKeyBase64 }}
</div>
<button
v-if="contact.seesMe"
class="tooltip"
@click="setVisibility(contact, false)"
>
<fa icon="eye" class="text-slate-900 fa-fw ml-1" />
<span class="tooltiptext">They can see you</span>
</button>
<button v-else class="tooltip" @click="setVisibility(contact, true)">
<span class="tooltiptext">They cannot see you</span>
<fa icon="eye-slash" class="text-slate-900 fa-fw ml-1" />
</button>
<button class="tooltip" @click="checkVisibility(contact)">
<span class="tooltiptext">Check Visibility</span>
<fa icon="rotate" class="text-slate-900 fa-fw ml-1" />
</button>
<button v-if="contact.registered" class="tooltip">
<span class="tooltiptext">Registered</span>
<fa icon="person-circle-check" class="text-slate-900 fa-fw ml-1" />
</button>
<button v-else @click="register(contact)" class="tooltip">
<span class="tooltiptext">Registration Unknown</span>
<fa
icon="person-circle-question"
class="text-slate-900 fa-fw ml-1"
/>
</button>
<button @click="deleteContact(contact)" class="px-9 tooltip">
<span class="tooltiptext">Delete!</span>
<fa icon="trash-can" class="text-red-600 fa-fw ml-1" />
</button>
<div v-if="showGiveNumbers" class="float-right">
<div class="float-right">
<div class="tooltip">
to:
{{
/* eslint-disable prettier/prettier */
this.showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span
v-if="givenByMeDescriptions[contact.did]"
class="tooltiptext-left"
>
{{ givenByMeDescriptions[contact.did] }}
</span>
<button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="onClickAddGive(activeDid, contact.did)"
>
+
</button>
</div>
<div class="tooltip px-2">
from:
{{
/* eslint-disable prettier/prettier */
this.showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span
v-if="givenToMeDescriptions[contact.did]"
class="tooltiptext-left"
>
{{ givenToMeDescriptions[contact.did] }}
</span>
<button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="onClickAddGive(contact.did, activeDid)"
>
+
</button>
</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>
</li>
</ul>
</section>
<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>
<script lang="ts">
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
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
const Buffer = require("buffer/").Buffer;
@Options({
components: {},
})
export default class ContactsView extends Vue {
activeDid = "";
contacts: Array<Contact> = [];
contactInput = "";
// { "did:...": concatenated-descriptions } entry for each contact
givenByMeDescriptions: Record<string, string> = {};
// { "did:...": amount } entry for each contact
givenByMeConfirmed: Record<string, number> = {};
// { "did:...": amount } entry for each contact
givenByMeUnconfirmed: Record<string, number> = {};
// { "did:...": concatenated-descriptions } entry for each contact
givenToMeDescriptions: Record<string, string> = {};
// { "did:...": amount } entry for each contact
givenToMeConfirmed: Record<string, number> = {};
// { "did:...": amount } entry for each contact
givenToMeUnconfirmed: Record<string, number> = {};
hourDescriptionInput = "";
hourInput = "0";
showGiveNumbers = false;
showGiveTotals = true;
showGiveConfirmed = true;
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.showGiveNumbers = !!settings?.showContactGivesInline;
if (this.showGiveNumbers) {
this.loadGives();
}
const allContacts = await db.contacts.toArray();
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
);
}
async loadGives() {
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(
"Attempted to load Give records with no identity available."
);
return;
}
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// load all the time I have given
try {
const url =
endorserApiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
console.log("All gifts you've given:", resp.data);
if (resp.status === 200) {
const contactDescriptions: Record<string, string> = {};
const contactConfirmed: Record<string, number> = {};
const contactUnconfirmed: Record<string, number> = {};
const allData: Array<GiveServerRecord> = resp.data.data;
for (const give of allData) {
if (give.unit == "HUR") {
const recipDid: string = give.recipientDid;
if (give.amountConfirmed) {
const prevAmount = contactConfirmed[recipDid] || 0;
contactConfirmed[recipDid] = prevAmount + give.amount;
} else {
const prevAmount = contactUnconfirmed[recipDid] || 0;
contactUnconfirmed[recipDid] = prevAmount + give.amount;
}
if (!contactDescriptions[recipDid] && give.description) {
// Since many make the tooltip too big, we'll just use the latest.
contactDescriptions[recipDid] = give.description;
}
}
}
//console.log("Done retrieving gives", contactConfirmed);
this.givenByMeDescriptions = contactDescriptions;
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) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
// load all the time I have received
try {
const url =
endorserApiServer +
"/api/v2/report/gives?recipientDid=" +
encodeURIComponent(identity.did);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
console.log("All gifts you've recieved:", resp.data);
if (resp.status === 200) {
const contactDescriptions: Record<string, string> = {};
const contactConfirmed: Record<string, number> = {};
const contactUnconfirmed: Record<string, number> = {};
const allData: Array<GiveServerRecord> = resp.data.data;
for (const give of allData) {
if (give.unit == "HUR") {
if (give.amountConfirmed) {
const prevAmount = contactConfirmed[give.agentDid] || 0;
contactConfirmed[give.agentDid] = prevAmount + give.amount;
} else {
const prevAmount = contactUnconfirmed[give.agentDid] || 0;
contactUnconfirmed[give.agentDid] = prevAmount + give.amount;
}
if (!contactDescriptions[give.agentDid] && give.description) {
// Since many make the tooltip too big, we'll just use the latest.
contactDescriptions[give.agentDid] = give.description;
}
}
}
//console.log("Done retrieving receipts", contactConfirmed);
this.givenToMeDescriptions = contactDescriptions;
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) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
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) {
if (
confirm(
"Are you sure you want to delete " +
this.nameForDid(this.contacts, contact.did) +
" with DID " +
contact.did +
" ?"
)
) {
await db.open();
await db.contacts.delete(contact.did);
this.contacts = R.without([contact], this.contacts);
}
}
async register(contact: Contact) {
if (
confirm(
"Are you sure you want to use one of your registrations for " +
this.nameForDid(this.contacts, contact.did) +
"?"
)
) {
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");
// Make a claim
const vcClaim: RegisterVerifiableCredential = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { identifier: identity.did },
object: SERVICE_ID,
recipient: { identifier: contact.did },
};
// 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) {
// 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?.handleId) {
contact.registered = true;
db.contacts.update(contact.did, { registered: true });
this.alertTitle = "Registration Success";
this.alertMessage = contact.name + " has been registered.";
this.isAlertVisible = true;
}
} 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;
}
}
}
}
async setVisibility(contact: Contact, visibility: boolean) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
"/api/report/" +
(visibility ? "canSeeMe" : "cannotSeeMe");
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");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const payload = JSON.stringify({ did: contact.did });
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.status === 200) {
contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility });
} else {
this.alertTitle = "Error With Server";
console.log("Bad response setting visibility: ", 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) {
this.alertTitle = "Error With Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
async checkVisibility(contact: Contact) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
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");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const visibility = resp.data;
contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility });
this.alertTitle = "Refreshed";
this.alertMessage =
this.nameForContact(contact, true) +
" can " +
(visibility ? "" : "not ") +
"see your activity.";
this.isAlertVisible = true;
} else {
this.alertTitle = "Error With Server";
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) {
this.alertTitle = "Error With Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
// from https://stackoverflow.com/a/175787/845494
//
private isNumeric(str: string): boolean {
return !isNaN(+str);
}
private nameForDid(contacts: Array<Contact>, did: string): string {
const contact = R.find((con) => con.did == did, contacts);
return this.nameForContact(contact);
}
private nameForContact(contact?: Contact, capitalize?: boolean): string {
return contact?.name || (capitalize ? "T" : "t") + "this unnamed user";
}
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)) {
this.alertTitle = "Input Error";
this.alertMessage =
"This is not a valid number of hours: " + this.hourInput;
this.isAlertVisible = true;
} else if (!parseFloat(this.hourInput)) {
this.alertTitle = "Input Error";
this.alertMessage = "Giving 0 hours does nothing.";
this.isAlertVisible = true;
} else if (!identity) {
this.alertTitle = "Status Error";
this.alertMessage = "No identity is available.";
this.isAlertVisible = true;
} else {
// ask to confirm amount
let toFrom;
if (fromDid == identity?.did) {
toFrom = "from you to " + this.nameForDid(this.contacts, toDid);
} else {
toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you";
}
let description;
if (this.hourDescriptionInput) {
description = " with description '" + this.hourDescriptionInput + "'";
} else {
description = " with no description";
}
if (
confirm(
"Are you sure you want to record " +
this.hourInput +
" hours " +
toFrom +
description +
"?"
)
) {
this.createAndSubmitGive(
identity,
fromDid,
toDid,
parseFloat(this.hourInput),
this.hourDescriptionInput
);
}
}
}
private async createAndSubmitGive(
identity: IIdentifier,
fromDid: string,
toDid: string,
amount: number,
description: string
): Promise<void> {
// Make a claim
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
agent: { identifier: fromDid },
object: { amountOfThisGood: amount, unitCode: "HUR" },
recipient: { identifier: toDid },
};
if (description) {
vcClaim.description = description;
}
// 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) {
// 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?.handleId) {
this.alertTitle = "Done";
this.alertMessage = "Successfully logged time to the server.";
this.isAlertVisible = true;
if (fromDid === identity.did) {
const newList = R.clone(this.givenByMeUnconfirmed);
newList[toDid] = (newList[toDid] || 0) + amount;
this.givenByMeUnconfirmed = newList;
} else {
const newList = R.clone(this.givenToMeConfirmed);
newList[fromDid] = (newList[fromDid] || 0) + amount;
this.givenToMeConfirmed = newList;
}
}
} 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;
}
}
}
public toggleShowGiveTotals() {
if (this.showGiveTotals) {
this.showGiveTotals = false;
this.showGiveConfirmed = true;
} else if (this.showGiveConfirmed) {
this.showGiveTotals = false; // stays the same
this.showGiveConfirmed = false;
} else {
this.showGiveTotals = true;
this.showGiveConfirmed = 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,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
public showGiveAmountsClassNames() {
return {
"bg-slate-500": this.showGiveTotals,
"bg-green-600": !this.showGiveTotals && this.showGiveConfirmed,
"bg-yellow-600": !this.showGiveTotals && !this.showGiveConfirmed,
};
}
}
</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;
}
/* How do we share with the above so code isn't duplicated? */
.tooltip .tooltiptext-left {
visibility: hidden;
width: 200px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
bottom: 0%;
right: 105%;
margin-left: -60px;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
}
.tooltip:hover .tooltiptext-left {
visibility: visible;
}
</style>

View File

@@ -1,44 +1,36 @@
<template> <template>
<!-- QUICK NAV --> <!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50"> <nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200">
<ul class="flex text-2xl p-2 gap-2"> <ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed --> <!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1" <a href="" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa ><fa icon="house-chimney" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
<!-- Search --> <!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white"> <li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link <a href="search.html" class="block text-center py-3 px-1"
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa ><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
<!-- Projects --> <!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="" class="block text-center py-3 px-1"
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa ><fa icon="folder-open" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
<!-- Contacts --> <!-- Commitments -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="" class="block text-center py-3 px-1"
:to="{ name: 'contacts' }" ><fa icon="hand" class="fa-fw"></fa
class="block text-center py-3 px-1" ></a>
><fa icon="users" class="fa-fw"></fa
></router-link>
</li> </li>
<!-- Profile --> <!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="account-view.html" class="block text-center py-3 px-1"
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa ><fa icon="circle-user" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@@ -1,165 +0,0 @@
<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 text-slate-400">
<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">
Help
</h1>
<div>
<h2 class="text-xl font-semibold">Introduction</h2>
<p>
This app is a window into data that you and your friends own, focused on
gifts and collaboration.
</p>
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
<p>
There are two parts to backup your data: the identifier secrets and the
other data such as settings, contacts, etc.
</p>
<div class="px-4">
<h2 class="text-xl font-semibold">
How do I backup my identifier (secret) data?
</h2>
<ul class="list-disc list-inside">
<li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
</li>
<li>
Click on "Backup Identifier Seed" and follow the instructions.
</li>
</ul>
<h2 class="text-xl font-semibold">
How do I backup my other (non-identifier-secret) data?
</h2>
<ul class="list-disc list-inside">
<li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
</li>
<li>
Click on "Download Settings...". That will save a file to your
downloads folder. That is your backup, so put it someplace where you
won't lose it.
</li>
</ul>
</div>
<h2 class="text-xl font-semibold">How do I restore my data?</h2>
<p>
There are two parts to restore your data: the identity secrets and the
other data such as settings, contacts, etc.
</p>
<div class="px-4">
<h2 class="text-xl font-semibold">
How do I restore my identifier (secret) data?
</h2>
<ul class="list-disc list-inside">
<li>
You only have one identifier at a time. If you have an identifier on
Your Identity <fa icon="circle-user" class="fa-fw" /> page, you'll
need to clear it out;
<a
href="https://www.lifewire.com/how-to-clear-cache-2617980"
class="text-blue-500"
>
here are some helpful instructions.
</a>
But beware! This will also clear out your settings and contact data,
so be sure to back that up first.
</li>
<li>
<router-link class="text-blue-500" to="/">
Go to the start
</router-link>
and choose "Yes" to enter the identity you backed up.
</li>
</ul>
<h2 class="text-xl font-semibold">
How do I restore my other (non-identifier-secret) data?
</h2>
<ul class="list-disc list-inside">
<li>Make sure you have your backup file (above), then contact us.</li>
</ul>
</div>
<h2 class="text-xl font-semibold">
How do I add someone to my contacts?
</h2>
<p>
Tell them to copy their ID, which typically starts with "did:ethr:...",
and send it to you. Go to the Contacts
<fa icon="circle-user" class="fa-fw" /> page and enter that into the top
form. You may add a name by adding a comma followed by their name; you
may also add their public key by adding another comma followed by the
key.
</p>
<h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info?
</h2>
<p>
Sometimes someone else has made claims -- meaning they have given time,
or recognized time given, or declared a project -- but you cannot see
anything associated with them. The reason may be because they have not
given you permission to see their information. Ask them to add you to
their contact list and make sure the eye next to your name is open like
this
<fa icon="eye" class="fa-fw" /> and not closed like this
<fa icon="eye-slash" class="fa-fw" />.
</p>
</div>
</section>
</template>

View File

@@ -4,99 +4,46 @@
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel --> <!-- Cancel -->
<button <a
@click="$router.go(-1)" href="start.html"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
> ><fa icon="chevron-left"></fa>
<fa icon="chevron-left"></fa> </a>
</button>
Import Existing Identity Import Existing Identity
</h1> </h1>
</div> </div>
<!-- Import Account Form --> <!-- Import Account Form -->
<p class="text-center text-xl mb-4 font-light"> <form>
Enter your seed phrase below to import your identity on this device. <p class="text-center text-xl mb-4 font-light">
</p> Enter your seed phrase below to import your identity on this device.
<input </p>
type="text" <input
placeholder="Seed Phrase" type="text"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" placeholder="Seed Phrase"
v-model="mnemonic" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/> />
{{ mnemonic }} <div class="mt-8">
<div class="mt-8"> <input
<button type="submit"
@click="from_mnemonic()" class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" value="Import Identity"
> />
Import <button
</button> type="button"
<button class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="onCancelClick()" >
type="button" Cancel
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" </button>
> </div>
Cancel </form>
</button>
</div>
</section> </section>
</template> </template>
<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 { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Options({ @Options({
components: {}, components: {},
}) })
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {}
mnemonic = "";
address = "";
privateHex = "";
publicHex = "";
derivationPath = "";
public onCancelClick() {
this.$router.back();
}
public async from_mnemonic() {
const mne: string = this.mnemonic.trim().toLowerCase();
if (this.mnemonic.trim().length > 0) {
[this.address, this.privateHex, this.publicHex, this.derivationPath] =
deriveAddress(mne);
const newId = newIdentifier(
this.address,
this.publicHex,
this.privateHex,
this.derivationPath
);
try {
await accountsDB.open();
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: this.derivationPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mne,
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" });
} catch (err) {
console.log("Error!");
console.log(err);
}
}
}
}
</script> </script>

View File

@@ -4,12 +4,11 @@
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel --> <!-- Cancel -->
<button <a
@click="$router.go(-1)" href="account-view.html"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
> ><fa icon="chevron-left" class="fa-fw"></fa>
<fa icon="chevron-left" class="fa-fw"></fa> </a>
</button>
[New/Edit] Identity [New/Edit] Identity
</h1> </h1>
</div> </div>
@@ -18,20 +17,17 @@
type="text" type="text"
placeholder="First Name" placeholder="First Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="firstName"
/> />
<input <input
type="text" type="text"
placeholder="Last Name" placeholder="Last Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="lastName"
/> />
<div class="mt-8"> <div class="mt-8">
<button <button
type="button" type="button"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
@click="onClickSaveChanges()"
> >
Save Changes Save Changes
</button> </button>
@@ -39,7 +35,6 @@
<button <button
type="button" type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="onClickCancel()"
> >
Cancel Cancel
</button> </button>
@@ -50,42 +45,9 @@
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Options, Vue } from "vue-class-component";
import { db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Options({ @Options({
components: {}, components: {},
}) })
export default class NewEditAccountView extends Vue { export default class NewEditAccountView extends Vue {}
firstName =
localStorage.getItem("firstName") === null
? "--"
: localStorage.getItem("firstName");
lastName =
localStorage.getItem("lastName") === null
? "--"
: localStorage.getItem("lastName");
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.firstName = settings?.firstName || "";
this.lastName = settings?.lastName || "";
}
onClickSaveChanges() {
db.settings.update(MASTER_SETTINGS_KEY, {
firstName: this.firstName,
lastName: this.lastName,
});
localStorage.setItem("firstName", this.firstName as string);
localStorage.setItem("lastName", this.lastName as string);
this.$router.push({ name: "account" });
}
onClickCancel() {
this.$router.back();
}
}
</script> </script>

View File

@@ -5,11 +5,11 @@
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel --> <!-- Cancel -->
<router-link <a
:to="{ name: 'project' }" href="project-view.html"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa> ><fa icon="chevron-left" class="fa-fw"></fa>
</router-link> </a>
Make Commitment Make Commitment
</h1> </h1>

View File

@@ -5,291 +5,74 @@
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel --> <!-- Cancel -->
<router-link <a
:to="{ name: 'project' }" href="project-view.html"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa ><fa icon="chevron-left" class="fa-fw"></fa>
></router-link> </a>
[New/Edit] Plan
[New/Edit] Project
</h1> </h1>
</div> </div>
<!-- Project Details --> <!-- Project Details -->
<!-- Image - (see design model) Empty --> <form>
<!-- Image - (see design model) Empty -->
<div> <!-- Image - Populated -->
{{ errorMessage }} <div class="relative mb-4 rounded-md overflow-hidden">
</div> <div class="absolute top-3 right-3 flex gap-2">
<button
class="text-md font-bold uppercase bg-blue-600 text-white px-3 py-2 rounded"
>
<fa icon="pen" class="fa-fw"></fa>
</button>
<button
class="text-md font-bold uppercase bg-red-600 text-white px-3 py-2 rounded"
>
<fa icon="trash-can" class="fa-fw"></fa>
</button>
</div>
<img src="https://picsum.photos/800/400" class="w-full" />
</div>
<input <input
type="text" type="text"
placeholder="Project Name" placeholder="Project Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="projectName" />
/>
<textarea <textarea
placeholder="Description" placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5" rows="5"
v-model="description" ></textarea>
maxlength="500" <div class="text-xs text-slate-500 italic -mt-3 mb-4">
></textarea> 88/500 max. characters
<div class="text-xs text-slate-500 italic -mt-3 mb-4"> </div>
{{ description.length }}/500 max. characters
</div>
<div class="mt-8"> <div class="mt-8">
<button <input
:disabled="isHiddenSave" type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
@click="onSaveProjectClick()" value="Save Project"
> />
<!-- SHOW if in idle state --> <button
<span :class="{ hidden: isHiddenSave }">Save Project</span> type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
<!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }"
><i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving&hellip;</span
> >
</button> Cancel
<button </button>
type="button" </div>
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" </form>
@click="onCancelClick()"
>
Cancel
</button>
</div>
</section> </section>
<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 { 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 { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core";
interface VerifiableCredential {
"@context": string;
"@type": string;
name: string;
description: string;
identifier?: string;
}
@Options({ @Options({
components: {}, components: {},
}) })
export default class NewEditProjectView extends Vue { export default class NewEditProjectView extends Vue {}
activeDid = "";
projectName = "";
description = "";
errorMessage = "";
alertTitle = "";
alertMessage = "";
projectId = localStorage.getItem("projectId") || "";
isHiddenSave = false;
isHiddenSpinner = true;
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
if (this.projectId) {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProject(identity);
}
}
}
async LoadProject(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
console.log(resp.status, resp.data);
if (resp.status === 200) {
const claim = resp.data.claim;
this.projectName = claim.name;
this.description = claim.description;
}
} catch (error) {
console.log(error);
}
}
private async SaveProject(identity: IIdentifier) {
// Make a claim
const vcClaim: VerifiableCredential = {
"@context": "https://schema.org",
"@type": "PlanAction",
name: this.projectName,
description: this.description,
identifier: this.projectId || undefined,
};
if (this.projectId) {
vcClaim.identifier = this.projectId;
}
// 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) {
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);
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
this.errorMessage = "";
this.alertTitle = "";
this.alertMessage = "";
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
useAppStore().setProjectId(
resp.data.success.handleId || resp.data.success.fullIri
);
setTimeout(
function (that: Vue) {
const route = {
name: "project",
};
console.log(route);
that.$router.push(route);
},
2000,
this
);
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
this.isAlertVisible = true;
if (serverError.message) {
this.alertTitle = "User Message";
userMessage = serverError.message; // This is info for the user.
this.alertMessage = userMessage;
} else {
this.alertTitle = "Server Message";
this.alertMessage = JSON.stringify(serverError.toJSON());
}
} else {
console.log("Here's the full error trying to save the claim:", error);
this.alertTitle = "Claim Error";
this.alertMessage = error as string;
}
// Now set that error for the user to see.
this.errorMessage = userMessage;
}
}
}
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public async onSaveProjectClick() {
this.isHiddenSave = true;
this.isHiddenSpinner = false;
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.SaveProject(identity);
}
}
public onCancelClick() {
this.$router.back();
}
isAlertVisible = false;
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"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>

View File

@@ -1,44 +1,36 @@
<template> <template>
<!-- QUICK NAV --> <!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50"> <nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200">
<ul class="flex text-2xl p-2 gap-2"> <ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed --> <!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1" <a href="" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa ><fa icon="house-chimney" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
<!-- Search --> <!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="search.html" class="block text-center py-3 px-1"
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa ><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
<!-- Projects --> <!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link <a href="" class="block text-center py-3 px-1"
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa ><fa icon="folder-open" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
<!-- Contacts --> <!-- Commitments -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="" class="block text-center py-3 px-1"
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="hand" class="fa-fw"></fa ><fa icon="hand" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
<!-- Profile --> <!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500"> <li class="basis-1/5 rounded-md text-slate-500">
<router-link <a href="account-view.html" class="block text-center py-3 px-1"
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa ><fa icon="circle-user" class="fa-fw"></fa
></router-link> ></a>
</li> </li>
</ul> </ul>
</nav> </nav>
@@ -49,12 +41,9 @@
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Back --> <!-- Back -->
<button <a href="" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)" ><fa icon="chevron-left" class="fa-fw"></fa
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" ></a>
>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
<!-- Context Menu --> <!-- Context Menu -->
<a <a
href="" href=""
@@ -62,57 +51,42 @@
><fa icon="ellipsis-vertical" class="fa-fw"></fa ><fa icon="ellipsis-vertical" class="fa-fw"></fa
></a> ></a>
View Plan View Project
</h1> </h1>
</div> </div>
<div>
{{ errorMessage }}
</div>
<!-- Project Details --> <!-- Project Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<!-- Image -->
<div class="-mx-4 -mt-3 mb-3">
<img src="https://picsum.photos/800/400" class="w-full" />
</div>
<div> <div>
<h2 class="text-xl font-semibold">{{ name }}</h2> <h2 class="text-xl font-semibold">Canyon cleanup</h2>
<div class="flex justify-between gap-4 text-sm mb-3"> <div class="flex justify-between gap-4 text-sm mb-3">
<span><fa icon="user" class="fa-fw text-slate-400"></fa> Rotary</span> <span><fa icon="user" class="fa-fw text-slate-400"></fa> Rotary</span>
<span <span
><fa icon="calendar" class="fa-fw text-slate-400"></fa ><fa icon="calendar" class="fa-fw text-slate-400"></fa> 8 days
>{{ timeSince }} ago</span
</span> >
</div> </div>
<div class="text-sm text-slate-500"> <div class="text-sm text-slate-500">
<div v-if="!expanded"> Sed ut perspiciatis unde omnis iste natus error sit voluptatem
{{ truncatedDesc }} accusantium doloremque laudantium
<a v-if="description.length >= truncateLength" @click="expandText" <a href="" class="uppercase text-xs font-semibold text-slate-700"
>Read More</a >Read More</a
> >
</div>
<div v-else>
{{ description }}
<a
@click="collapseText"
class="uppercase text-xs font-semibold text-slate-700"
>Read Less</a
>
</div>
</div> </div>
</div> </div>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="onEditClick()"
>
Edit
</button>
</div> </div>
<!-- Commit --> <!-- Commit -->
<router-link <a
:to="{ name: 'new-edit-commitment' }" href="commitment-edit.html"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8" class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
>Make Commitment</router-link >Make Commitment</a
> >
<!-- Commitments --> <!-- Commitments -->
@@ -146,103 +120,10 @@
</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 { IIdentifier } from "@veramo/core";
@Options({ @Options({
components: {}, components: {},
}) })
export default class ProjectViewView extends Vue { export default class ProjectViewView extends Vue {}
expanded = false;
name = "";
description = "";
truncatedDesc = "";
truncateLength = 40;
timeSince = "";
projectId = localStorage.getItem("projectId") || "";
errorMessage = "";
onEditClick() {
localStorage.setItem("projectId", this.projectId as string);
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
expandText() {
this.expanded = true;
}
collapseText() {
this.expanded = false;
}
async LoadProject(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// eslint-disable-next-line prettier/prettier
const url = endorserApiServer + "/api/claim/byHandle/" + encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
console.log("resp.status, resp.data", resp.status, resp.data);
if (resp.status === 200) {
const startTime = resp.data.startTime;
if (startTime != null) {
const eventDate = new Date(startTime);
const now = moment.now();
this.timeSince = moment.utc(now).to(eventDate);
}
this.name = resp.data.claim?.name || "(no name)";
this.description = resp.data.claim?.description || "(no description)";
this.truncatedDesc = this.description.slice(0, this.truncateLength);
} else if (resp.status === 404) {
// actually, axios throws an error so we never get here
this.errorMessage = "That project does not exist.";
}
} catch (error: unknown) {
const serverError = error as AxiosError;
if (serverError.response?.status === 404) {
this.errorMessage = "That project does not exist.";
} else {
this.errorMessage =
"Something went wrong retrieving that project." +
" See logs for more info.";
console.log("Error retrieving project:", error);
}
}
}
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProject(identity);
}
}
}
</script> </script>

View File

@@ -1,186 +0,0 @@
<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 bg-slate-400 text-white">
<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">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Plans
</h1>
<!-- Quick Search -->
<form id="QuickSearch" class="mb-4 flex">
<input
type="text"
placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/>
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</button>
</form>
<!-- New Project -->
<button
class="fixed right-6 bottom-24 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full"
@click="onClickNewProject()"
>
<fa icon="plus" class="fa-fw"></fa>
</button>
<!-- Results List -->
<ul class="">
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm truncate">
{{ project.description }}
</div>
</div>
</a>
</li>
</ul>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
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 { IIdentifier } from "@veramo/core";
@Options({
components: {},
})
export default class ProjectsView extends Vue {
projects: { handleId: string; name: string; description: string }[] = [];
onClickLoadProject(id: string) {
console.log("projectId", id);
localStorage.setItem("projectId", id);
const route = {
name: "project",
};
console.log(route);
this.$router.push(route);
}
async LoadProjects(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/report/plansByIssuer";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const plans = resp.data.data;
for (let i = 0; i < plans.length; i++) {
const plan = plans[i];
const data = {
name: plan.name,
description: plan.description,
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
handleId: plan.handleId || plan.fullIri,
};
this.projects.push(data);
}
}
} catch (error) {
console.log(error);
}
}
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProjects(identity);
}
}
onClickNewProject(): void {
localStorage.removeItem("projectId");
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
}
</script>

View File

@@ -12,16 +12,16 @@
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">
Do you already have an identity to import? Do you already have an identity to import?
</p> </p>
<a <router-link
@click="onClickYes()" :to="{ name: 'new-edit-account' }"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
> >
No No
</a> </router-link>
<a <router-link
@click="onClickNo()" :to="{ name: 'import-account' }"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>Yes</a >Yes</router-link
> >
</div> </div>
</section> </section>
@@ -33,13 +33,5 @@ import { Options, Vue } from "vue-class-component";
@Options({ @Options({
components: {}, components: {},
}) })
export default class StartView extends Vue { export default class StartView extends Vue {}
public onClickYes() {
this.$router.push({ name: "account" });
}
public onClickNo() {
this.$router.push({ name: "import-account" });
}
}
</script> </script>

View File

@@ -1,10 +1,4 @@
const { defineConfig } = require("@vue/cli-service"); const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true, transpileDependencies: true,
configureWebpack: {
devtool: "source-map",
experiments: {
topLevelAwait: true,
},
},
}); });