Compare commits
269 Commits
502352ad36
...
friend-tec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7f15fde25 | ||
| 2db662c125 | |||
| b7892f4dfa | |||
|
|
3bbb138299 | ||
|
|
5b5c631001 | ||
|
|
e60b56a0b0 | ||
|
|
d3e025c293 | ||
|
|
6f4027f614 | ||
| 249811efe3 | |||
| bd2455458f | |||
| a053c48819 | |||
| 9486142b2a | |||
|
|
2fba7f2a55 | ||
|
|
31d13b9143 | ||
|
|
852bd93f3f | ||
| b707bfce40 | |||
| bdb8e2e32a | |||
| 06b173e861 | |||
| 6a8b9d36a7 | |||
| 52a6451a2d | |||
| 4b9cbd0e9f | |||
| a5e0c847b1 | |||
|
|
fd43da93a5 | ||
| b59bcf249a | |||
| b05b602acd | |||
| b8aaffbf8d | |||
|
|
5501ac1a2f | ||
|
|
b514d64068 | ||
|
|
c4537420b4 | ||
|
|
5f50338dd0 | ||
| 308386d829 | |||
| 999d7abc04 | |||
| f7f947bfdd | |||
| 26d9b134c7 | |||
| 43f942c905 | |||
| 8ee610c1bc | |||
| 8d15b7bfb8 | |||
| 5c57ee3e72 | |||
|
|
3f7bcbfd76 | ||
| ef0988c9ec | |||
| 22de6113e9 | |||
| 87139f203c | |||
| c8de13d376 | |||
| 2ccfb283b4 | |||
| 552fce3281 | |||
| 12de3dec4f | |||
| b171e1ae13 | |||
| dc54006fca | |||
| 9b4db018f5 | |||
| 519f320a2e | |||
|
|
f1b3094026 | ||
|
|
e5ad87f4d5 | ||
|
|
7de6171911 | ||
|
|
bb6bacac97 | ||
|
|
40fc6a29a4 | ||
|
|
9ec19fa4ee | ||
| 28b20f86ea | |||
| 502109de4b | |||
| 97274a701d | |||
| 81a6d73f2f | |||
| 5804f692b7 | |||
| 257aa8d49e | |||
| 34806b514b | |||
| 0024238ca8 | |||
| 0af05b4b0d | |||
| b9d59eb642 | |||
| 0c05505c46 | |||
| 98c093f655 | |||
| 88112e0629 | |||
|
|
6ab92a83bd | ||
|
|
bfc52151c0 | ||
|
|
868b5413de | ||
|
|
50005a0dc3 | ||
|
|
9247b6ed1f | ||
|
|
75f26ccf2d | ||
|
|
bfd2498906 | ||
|
|
4933017e9c | ||
|
|
18c23451bb | ||
|
|
304985f88d | ||
|
|
9a41aff8f0 | ||
|
|
e19cd980b4 | ||
| 6d1756b4a5 | |||
| ac4c92d8e8 | |||
| 937a3cb6c6 | |||
| 194f741984 | |||
| b31c0d975c | |||
| f09684d7cd | |||
| 1767a48a7f | |||
| d2e2fc707e | |||
| bf6830a1a8 | |||
|
|
fe09f5180d | ||
|
|
5addc3c206 | ||
|
|
69f2f3cfd2 | ||
|
|
4de66b1609 | ||
|
|
4b87692231 | ||
|
|
503bb1bd93 | ||
|
|
9fa3b8be0b | ||
| 3b1a9b9c5b | |||
|
|
f55e50067f | ||
| 7f48149d6f | |||
| c5b4921583 | |||
| b28689ad06 | |||
| 0444b5be32 | |||
| 4866416aae | |||
| e48a4ed05b | |||
| 87cfead094 | |||
| 179a5cd9f8 | |||
|
|
eff67c2a4a | ||
|
|
db22d559b7 | ||
|
|
c4443f2ed1 | ||
| be348461f1 | |||
| 6e2c596030 | |||
|
|
05a7758c65 | ||
|
|
c502869c5f | ||
|
|
b7aacd63e6 | ||
|
|
5bc0e27b30 | ||
|
|
a4fe94f081 | ||
|
|
8de95566df | ||
|
|
97569697f6 | ||
|
|
b9ed9d748b | ||
|
|
790d44db81 | ||
| e2bf469dc1 | |||
| d0ec7930e1 | |||
| 3e2cd1291c | |||
| 8d42fe905d | |||
| 592ffacebc | |||
| b706e65598 | |||
|
|
6e3066ae92 | ||
| a7e98c8f1a | |||
| f07c804b24 | |||
|
|
e8eae544f3 | ||
|
|
7c77578f79 | ||
|
|
34636d6047 | ||
|
|
5134e2f562 | ||
| 91b46eaaee | |||
| 31d1a449ae | |||
| 1248132076 | |||
| 015704c94e | |||
| 540ef916c2 | |||
| bee7c87a8f | |||
| 6bbc88f86c | |||
| 624abbb179 | |||
| 110ed009b2 | |||
| a5892238d5 | |||
| 8eb80a9ede | |||
|
|
32125133f0 | ||
|
|
47ade49e31 | ||
|
|
47ce91cca1 | ||
|
|
3e52b504b0 | ||
|
|
4ecea1ab0e | ||
|
|
b9fdc920ea | ||
|
|
0907d59a6a | ||
| 59ce15c744 | |||
|
|
9960a96a20 | ||
|
|
098c6c0fa0 | ||
|
|
ead37ede74 | ||
|
|
f428199228 | ||
|
|
1405b88323 | ||
|
|
44fc2850dd | ||
|
|
52d411470e | ||
|
|
ab678a900a | ||
|
|
efa59e170f | ||
|
|
7a4ceaa455 | ||
| d7a9fb6d54 | |||
| 78b98bab5e | |||
| 2493f2ad39 | |||
| cf2b80b1f5 | |||
| 00954693b5 | |||
| 2dd77f898f | |||
| c1f218c2f3 | |||
| b5e78e5dc8 | |||
| b86323ec83 | |||
| 8add6448fb | |||
| 47442655cb | |||
|
|
1d362c314b | ||
| 3eda246e85 | |||
|
|
3f13d3ea33 | ||
|
|
cef346e487 | ||
|
|
fed23a61ee | ||
|
|
b6b7c56157 | ||
| 3f8be3b4de | |||
| 21af37c2c2 | |||
| 0b7a35c9b8 | |||
| 0257901c5b | |||
| d9d6096275 | |||
| ed7d37c649 | |||
| 81dd6eb595 | |||
|
|
c61bb88788 | ||
|
|
3bd55f3ad2 | ||
|
|
3471afdf25 | ||
|
|
e25a83ff1b | ||
|
|
0fbdb45d3e | ||
|
|
dc23ba1375 | ||
|
|
08137eb000 | ||
|
|
5d49965166 | ||
| 8e8aa4356d | |||
| 59a354027e | |||
|
|
5dc80ce12a | ||
|
|
754bced2a9 | ||
|
|
e3f58bd593 | ||
|
|
3b41014083 | ||
| f568149745 | |||
| a27d035e9b | |||
| 16d0be681c | |||
|
|
5be67fd4c9 | ||
|
|
dda3ad057d | ||
|
|
cf54096326 | ||
| 49c3971cf2 | |||
|
|
80a1185faa | ||
| cd8bc73bac | |||
| e42b3ff11d | |||
| d98e95915b | |||
|
|
4758a740de | ||
|
|
0a020a4069 | ||
|
|
c859778832 | ||
|
|
c24022c41c | ||
|
|
0fd4b86a84 | ||
|
|
c31445865e | ||
|
|
0af03227a6 | ||
|
|
3c977a1f28 | ||
|
|
8d8635a3e6 | ||
|
|
bcc6de6fc0 | ||
|
|
99ea161da0 | ||
|
|
3f6dbdebef | ||
|
|
b139957e3e | ||
| 6e4f6d090a | |||
| 48227e8cf2 | |||
| 09f02ca4b2 | |||
| 9b3823ef0e | |||
| cdeece1795 | |||
| c2ebaa0a76 | |||
| 3f60051599 | |||
| a8f1e25986 | |||
| 964248e895 | |||
| a2b3cebdb3 | |||
| bc6e52774c | |||
| 643f777d10 | |||
| ec1d8404ca | |||
|
|
1d6241abbb | ||
| c40b690878 | |||
| c9c81f1e5c | |||
| a94069e70a | |||
|
|
53f42e1ad3 | ||
|
|
5f0bbccbe6 | ||
|
|
3ec9056901 | ||
|
|
6d3ab7c313 | ||
| d9e9a7b740 | |||
| ea95382fdf | |||
| 072b663ec9 | |||
|
|
6393a20e7e | ||
|
|
19d934eb28 | ||
|
|
49ce7d43b0 | ||
|
|
6233189a49 | ||
|
|
ffb0f2d37f | ||
| db7b3fff06 | |||
| bd75802a0c | |||
| 1a86730354 | |||
| a96728bec5 | |||
| 944b0ad759 | |||
| 42bf34f549 | |||
|
|
071c41b70c | ||
| 5747404fd6 | |||
| 639f630436 | |||
| a8794be2ea | |||
|
|
0726a8d3ba | ||
| aa2f484a9f | |||
| 07e7a70d56 | |||
| 6daa515d19 | |||
| d5336dbf1b |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,13 +1,16 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
|
signature.bin
|
||||||
|
*.pem
|
||||||
|
verified.txt
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
# Log files
|
# Log filesopenssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -11,6 +11,9 @@ npm run serve
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
### Compiles and minifies for production
|
||||||
|
|
||||||
|
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
@@ -20,11 +23,6 @@ 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
|
### Test key contents
|
||||||
|
|
||||||
See [this page](openssl_signing_console.rst)
|
See [this page](openssl_signing_console.rst)
|
||||||
@@ -59,17 +57,35 @@ playing one of two ways:
|
|||||||
|
|
||||||
### Create multiple identifiers
|
### Create multiple identifiers
|
||||||
|
|
||||||
Go to /import-account and import a new one. Then switch identifiers on the
|
Go to /start and create or import a new one. Then switch identifiers on the bottom of the Your Identity page.
|
||||||
bottom of the Your Identity page.
|
|
||||||
|
|
||||||
### Create keys with alternate tools
|
### Create keys with alternate tools
|
||||||
|
|
||||||
See [this page](openssl_signing_console.rst)
|
See [this page](openssl_signing_console.rst)
|
||||||
|
|
||||||
### Customize configuration
|
### Customize Vue configuration
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
|
|
||||||
|
|
||||||
|
## Scenarios
|
||||||
|
|
||||||
|
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
|
||||||
|
|
||||||
|
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this 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).)
|
||||||
|
|
||||||
|
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
|
||||||
|
|
||||||
|
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
|
||||||
|
|
||||||
|
### 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).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
See https://tea.xyz
|
See https://tea.xyz
|
||||||
@@ -83,6 +99,9 @@ See https://tea.xyz
|
|||||||
|
|
||||||
### Reference Material
|
### Reference Material
|
||||||
|
|
||||||
|
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
|
||||||
|
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
|
||||||
|
|
||||||
```
|
```
|
||||||
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
|
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
|
||||||
|
|
||||||
@@ -188,6 +207,9 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
|
|||||||
|
|
||||||
## Kudos
|
## Kudos
|
||||||
|
|
||||||
|
Gifts make the world go 'round!
|
||||||
|
|
||||||
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
|
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
|
||||||
* [Many libraries]() such as Veramo.io, Vuejs.org, threejs
|
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
||||||
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
|
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
|
||||||
|
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
jq
|
||||||
|
|
||||||
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:
|
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:
|
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
||||||
|
|||||||
25
openssl_signing_console.sh
Executable file
25
openssl_signing_console.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
||||||
|
openssl ec -in private.pem -pubout -out public.pem
|
||||||
|
|
||||||
|
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
||||||
|
|
||||||
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
||||||
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
||||||
|
|
||||||
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
|
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
|
||||||
|
|
||||||
|
# Read binary signature from file and encode it to Base64 URL-Safe format
|
||||||
|
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
|
# Construct the JWT
|
||||||
|
jwt="$signing_input.$signature_b64"
|
||||||
|
|
||||||
|
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
|
||||||
|
|
||||||
|
|
||||||
36418
package-lock.json
generated
36418
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
73
package.json
73
package.json
@@ -12,54 +12,56 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^2.0.5",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@tweenjs/tween.js": "^20.0.3",
|
"@tweenjs/tween.js": "^21.0.0",
|
||||||
"@veramo/core": "^5.1.2",
|
"@veramo/core": "^5.2.0",
|
||||||
"@veramo/credential-w3c": "^5.1.4",
|
"@veramo/credential-w3c": "^5.2.0",
|
||||||
"@veramo/data-store": "^5.1.2",
|
"@veramo/data-store": "^5.2.0",
|
||||||
"@veramo/did-manager": "^5.1.2",
|
"@veramo/did-manager": "^5.1.2",
|
||||||
"@veramo/did-provider-ethr": "^5.1.2",
|
"@veramo/did-provider-ethr": "^5.1.2",
|
||||||
"@veramo/did-resolver": "^5.1.2",
|
"@veramo/did-resolver": "^5.2.0",
|
||||||
"@veramo/key-manager": "^5.1.2",
|
"@veramo/key-manager": "^5.1.2",
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^10.2.1",
|
||||||
"@zxing/text-encoding": "^0.9.0",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
"axios": "^1.3.4",
|
"axios": "^1.4.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"core-js": "^3.29.1",
|
"core-js": "^3.31.1",
|
||||||
"dexie": "^3.2.3",
|
"dexie": "^3.2.4",
|
||||||
"dexie-export-import": "^4.0.7",
|
"dexie-export-import": "^4.0.7",
|
||||||
"did-jwt": "^6.11.5",
|
"did-jwt": "^7.2.4",
|
||||||
"ethereum-cryptography": "^1.2.0",
|
"ethereum-cryptography": "^2.0.0",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"ethr-did-resolver": "^8.0.0",
|
"ethr-did-resolver": "^8.0.0",
|
||||||
"js-generate-password": "^0.1.7",
|
"jdenticon": "^3.2.0",
|
||||||
|
"js-generate-password": "^0.1.9",
|
||||||
"localstorage-slim": "^2.4.0",
|
"localstorage-slim": "^2.4.0",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
"merkletreejs": "^0.3.9",
|
"merkletreejs": "^0.3.10",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
"pinia-plugin-persistedstate": "^3.1.0",
|
"pinia-plugin-persistedstate": "^3.1.0",
|
||||||
"qr-code-generator-vue3": "^1.4.21",
|
"qr-code-generator-vue3": "^1.4.21",
|
||||||
"ramda": "^0.28.0",
|
"ramda": "^0.29.0",
|
||||||
"readable-stream": "^4.3.0",
|
"readable-stream": "^4.4.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"three": "^0.152.2",
|
"three": "^0.154.0",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-class-component": "^8.0.0-0",
|
"vue-facing-decorator": "^2.1.20",
|
||||||
"vue-facing-decorator": "^2.1.19",
|
"vue-router": "^4.2.3",
|
||||||
"vue-property-decorator": "^9.1.2",
|
"web-did-resolver": "^2.0.27"
|
||||||
"vue-router": "^4.1.6",
|
|
||||||
"web-did-resolver": "^2.0.22"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ramda": "^0.28.23",
|
"@types/leaflet": "^1.9.4",
|
||||||
"@types/three": "^0.152.0",
|
"@types/ramda": "^0.29.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/parser": "^5.57.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
"@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",
|
||||||
@@ -67,15 +69,16 @@
|
|||||||
"@vue/cli-plugin-typescript": "~5.0.8",
|
"@vue/cli-plugin-typescript": "~5.0.8",
|
||||||
"@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.3",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.37.0",
|
"eslint": "^8.44.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.10.0",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"postcss": "^8.4.21",
|
"leaflet": "^1.9.4",
|
||||||
"prettier": "^2.8.7",
|
"postcss": "^8.4.24",
|
||||||
"tailwindcss": "^3.3.1",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "~5.0.3"
|
"tailwindcss": "^3.3.2",
|
||||||
|
"typescript": "~5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,82 @@
|
|||||||
|
|
||||||
- top screens from img/screens.pdf milestone:2 :
|
tasks:
|
||||||
- view all :
|
|
||||||
- add infinite scroll assignee:matthew
|
|
||||||
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
|
||||||
|
|
||||||
- allow type annotations in World.js & landmarks.js (since we get this error: "Types are not supported by current JavaScript version")
|
- 08 Scan QR code to import into contacts assignee:matthew
|
||||||
- replace user-affecting console.log & console.error with error messages (eg. catches)
|
- SEE - https://github.com/gruhn/vue-qrcode-reader
|
||||||
|
|
||||||
- 8 Move to vue-facing-decorator
|
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
|
||||||
|
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
|
||||||
|
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
|
||||||
|
- 40 notifications :
|
||||||
|
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||||
|
|
||||||
|
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
||||||
|
|
||||||
|
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
||||||
|
|
||||||
|
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
|
||||||
|
|
||||||
|
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s) assignee-group:ui
|
||||||
|
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui
|
||||||
|
|
||||||
|
- Home Feed & Quick Give screen :
|
||||||
|
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
||||||
|
- 01 quick action - send action, maybe choose via canvas tool
|
||||||
|
- SEE: https://github.com/konvajs/vue-konva
|
||||||
|
|
||||||
|
- 24 Move to Vite assignee:matthew
|
||||||
|
|
||||||
|
- .2 Edit Plan does not have icons across the bottom assignee-group:ui
|
||||||
|
- .5 include the hash of the latest commit, and maybe a version
|
||||||
|
- .5 add link to further project / people when a project pays ahead
|
||||||
|
- .5 add project ID to the URL, to make a project publicly-accessible
|
||||||
|
- .5 remove edit from project page for projects owned by others
|
||||||
|
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
|
||||||
|
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist assignee-group:ui
|
||||||
|
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui
|
||||||
|
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
||||||
|
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
||||||
|
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||||
|
|
||||||
|
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
||||||
|
|
||||||
|
- 04 allow user to download claims, mine + ones I can see about me from others
|
||||||
|
- .5 change the derivation path, and regenerate test IDs
|
||||||
|
- 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path)
|
||||||
|
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages assignee-group:ui
|
||||||
|
- .5 customize favicon assignee-group:ui
|
||||||
|
- .5 Do we want to combine first name & last name?
|
||||||
|
- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui
|
||||||
|
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
||||||
|
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
|
||||||
|
|
||||||
|
- contacts v+ :
|
||||||
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
|
- .2 show error to user when adding a duplicate contact
|
||||||
|
- 01 parse input more robustly (with CSV lib and not commas)
|
||||||
|
|
||||||
- stats v1 :
|
- stats v1 :
|
||||||
- 01 show numeric stats
|
- 01 show numeric stats
|
||||||
- 01 link to world for specific stats
|
- 01 link to world for specific stats
|
||||||
|
- .5 don't load another instance of a bush if it already exists
|
||||||
- contacts v1 :
|
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
||||||
- 01 Import contact info a la QR code.
|
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
|
||||||
- .2 move all "identity" references to temporary account access
|
|
||||||
|
|
||||||
- contacts v+ :
|
|
||||||
- .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 Change "nav" tabs across the bottom into a component (eliminating duplicate code).
|
|
||||||
- .5 Fix how icons show on top of bottom bar on ContactAmounts page
|
|
||||||
|
|
||||||
- show pop-up confirming that settings & contacts have been downloaded
|
|
||||||
|
|
||||||
- Ensure each action sent to the server has a confirmation.
|
|
||||||
|
|
||||||
- discover screen
|
|
||||||
|
|
||||||
- .5 customize favicon
|
|
||||||
- .5 make advanced features harder to access; advanced build?
|
|
||||||
|
|
||||||
- 24 Move to Vite
|
|
||||||
|
|
||||||
- Release Minimum Viable Product :
|
- Release Minimum Viable Product :
|
||||||
|
- 08 thorough testing for errors & edge cases
|
||||||
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
||||||
- Add disclaimers.
|
- Add disclaimers.
|
||||||
- Switch default server to the public server.
|
- Switch default server to the public server.
|
||||||
- Deploy to a server.
|
- Deploy to a server.
|
||||||
- Ensure public server has limits that work for group adoption.
|
- Ensure public server has limits that work for group adoption.
|
||||||
- Test PWA features on Android and iOS.
|
- Test PWA features on Android and iOS.
|
||||||
|
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
||||||
|
|
||||||
|
- linking between projects or plans :
|
||||||
|
- show total time given to & from a project
|
||||||
|
- terminology:
|
||||||
|
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
||||||
|
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
|
||||||
|
|
||||||
- Stats :
|
- Stats :
|
||||||
- 01 point out user's location on the world
|
- 01 point out user's location on the world
|
||||||
@@ -55,11 +86,6 @@
|
|||||||
- badge for amount given/offered to your project
|
- badge for amount given/offered to your project
|
||||||
- set a goal of given/offers
|
- set a goal of given/offers
|
||||||
|
|
||||||
- linking between projects or plans :
|
|
||||||
- terminology:
|
|
||||||
- Fulfills, Feeds, contributes to, supplies, boosts, advances
|
|
||||||
- Precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
|
|
||||||
|
|
||||||
- automated tests, eg. cypress
|
- automated tests, eg. cypress
|
||||||
|
|
||||||
- Notifications (wake on the phone, push notifications)
|
- Notifications (wake on the phone, push notifications)
|
||||||
@@ -70,10 +96,17 @@
|
|||||||
|
|
||||||
- Peer DID
|
- Peer DID
|
||||||
|
|
||||||
|
|
||||||
- DIDComm
|
- DIDComm
|
||||||
|
|
||||||
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
|
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
|
||||||
|
|
||||||
- v video for multiple identities https://youtu.be/p8L87AeD76w
|
- Do we want split first name & last name?
|
||||||
- v video for adding time to contacts https://youtu.be/7Yylczevp10
|
|
||||||
|
- 40 notifications v+ :
|
||||||
|
- pull, w/ scheduled runs
|
||||||
|
|
||||||
|
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
||||||
|
|
||||||
|
log:
|
||||||
|
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
||||||
|
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 MiB |
BIN
public/img/textures/leafy-autumn-forest-floor.jpg
Normal file
BIN
public/img/textures/leafy-autumn-forest-floor.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 KiB |
177
sample.txt
Normal file
177
sample.txt
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
|
||||||
|
> kickstart-for-time-pwa@0.1.0 build
|
||||||
|
> vue-cli-service build
|
||||||
|
|
||||||
|
All browser targets in the browserslist configuration have supported ES module.
|
||||||
|
Therefore we don't build two separate bundles for differential loading.
|
||||||
|
|
||||||
|
|
||||||
|
WARNING Compiled with 5 warnings6:06:43 PM
|
||||||
|
|
||||||
|
[eslint]
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/components/World/components/objects/landmarks.js
|
||||||
|
98:11 warning Unexpected console statement no-console
|
||||||
|
133:7 warning Unexpected console statement no-console
|
||||||
|
144:5 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/router/index.ts
|
||||||
|
210:3 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/AccountViewView.vue
|
||||||
|
362:7 warning Unexpected console statement no-console
|
||||||
|
375:7 warning Unexpected console statement no-console
|
||||||
|
404:7 warning Unexpected console statement no-console
|
||||||
|
516:7 warning Unexpected console statement no-console
|
||||||
|
536:7 warning Unexpected console statement no-console
|
||||||
|
630:5 warning Unexpected console statement no-console
|
||||||
|
682:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactAmountsView.vue
|
||||||
|
206:9 warning Unexpected console statement no-console
|
||||||
|
233:9 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactGiftingView.vue
|
||||||
|
244:9 warning Unexpected console statement no-console
|
||||||
|
267:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactsView.vue
|
||||||
|
340:9 warning Unexpected console statement no-console
|
||||||
|
577:9 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/DiscoverView.vue
|
||||||
|
315:9 warning Unexpected console statement no-console
|
||||||
|
343:7 warning Unexpected console statement no-console
|
||||||
|
390:9 warning Unexpected console statement no-console
|
||||||
|
423:7 warning Unexpected console statement no-console
|
||||||
|
532:9 warning Unexpected console statement no-console
|
||||||
|
575:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/HomeView.vue
|
||||||
|
349:9 warning Unexpected console statement no-console
|
||||||
|
498:9 warning Unexpected console statement no-console
|
||||||
|
521:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/IdentitySwitcherView.vue
|
||||||
|
142:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportAccountView.vue
|
||||||
|
123:9 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportDerivedAccountView.vue
|
||||||
|
159:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/NewEditProjectView.vue
|
||||||
|
183:9 warning Unexpected console statement no-console
|
||||||
|
215:7 warning Unexpected console statement no-console
|
||||||
|
297:13 warning Unexpected console statement no-console
|
||||||
|
320:11 warning Unexpected console statement no-console
|
||||||
|
345:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectViewView.vue
|
||||||
|
387:9 warning Unexpected console statement no-console
|
||||||
|
421:7 warning Unexpected console statement no-console
|
||||||
|
457:7 warning Unexpected console statement no-console
|
||||||
|
552:9 warning Unexpected console statement no-console
|
||||||
|
554:11 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectsView.vue
|
||||||
|
131:9 warning Unexpected console statement no-console
|
||||||
|
144:7 warning Unexpected console statement no-console
|
||||||
|
221:9 warning Unexpected console statement no-console
|
||||||
|
237:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/SeedBackupView.vue
|
||||||
|
94:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
✖ 44 problems (0 errors, 44 warnings)
|
||||||
|
|
||||||
|
|
||||||
|
You may use special comments to disable some warnings.
|
||||||
|
Use // eslint-disable-next-line to ignore the next line.
|
||||||
|
Use /* eslint-disable */ to ignore all warnings in a file.
|
||||||
|
warning
|
||||||
|
|
||||||
|
/models/lupine_plant/textures/lambert2SG_baseColor.png is 3.75 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit.
|
||||||
|
|
||||||
|
warning
|
||||||
|
|
||||||
|
/models/lupine_plant/textures/lambert2SG_normal.png is 4.91 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit.
|
||||||
|
|
||||||
|
warning
|
||||||
|
|
||||||
|
asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
|
||||||
|
This can impact web performance.
|
||||||
|
Assets:
|
||||||
|
js/project.44f30c9f.js (318 KiB)
|
||||||
|
js/statistics.8a97010a.js (586 KiB)
|
||||||
|
js/chunk-vendors.a4845bfb.js (411 KiB)
|
||||||
|
js/705.f6a6ce2a.js (252 KiB)
|
||||||
|
img/textures/leafy-autumn-forest-floor.jpg (705 KiB)
|
||||||
|
models/lupine_plant/textures/lambert2SG_baseColor.png (3.58 MiB)
|
||||||
|
models/lupine_plant/textures/lambert2SG_normal.png (4.69 MiB)
|
||||||
|
|
||||||
|
warning
|
||||||
|
|
||||||
|
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
|
||||||
|
Entrypoints:
|
||||||
|
app (447 KiB)
|
||||||
|
js/chunk-vendors.a4845bfb.js
|
||||||
|
css/app.8f21529c.css
|
||||||
|
js/app.8833cebc.js
|
||||||
|
|
||||||
|
|
||||||
|
File Size Gzipped
|
||||||
|
|
||||||
|
dist/js/statistics.8a97010a.js 585.72 KiB 148.80 KiB
|
||||||
|
dist/js/chunk-vendors.a4845bfb.js 411.44 KiB 137.82 KiB
|
||||||
|
dist/js/project.44f30c9f.js 317.61 KiB 78.67 KiB
|
||||||
|
dist/js/705.f6a6ce2a.js 251.66 KiB 87.12 KiB
|
||||||
|
dist/js/891.33615e4f.js 147.32 KiB 42.09 KiB
|
||||||
|
dist/js/153.e2c8e249.js 146.26 KiB 42.21 KiB
|
||||||
|
dist/js/820.13565d16.js 66.10 KiB 18.33 KiB
|
||||||
|
dist/js/contact-qr.e170ec33.js 54.85 KiB 15.63 KiB
|
||||||
|
dist/js/772.7b4c53a7.js 30.29 KiB 7.21 KiB
|
||||||
|
dist/js/361.898a4525.js 27.40 KiB 8.19 KiB
|
||||||
|
dist/js/account.77d86130.js 17.51 KiB 5.93 KiB
|
||||||
|
dist/js/app.8833cebc.js 17.31 KiB 5.84 KiB
|
||||||
|
dist/js/contacts.3fc90ff8.js 16.94 KiB 5.52 KiB
|
||||||
|
dist/js/discover.24106939.js 15.30 KiB 5.22 KiB
|
||||||
|
dist/js/536.3bb13201.js 15.23 KiB 4.84 KiB
|
||||||
|
dist/workbox-5b385ed2.js 14.11 KiB 4.93 KiB
|
||||||
|
dist/js/home.218b99dd.js 13.89 KiB 4.97 KiB
|
||||||
|
dist/js/help.50d3117b.js 12.49 KiB 4.38 KiB
|
||||||
|
dist/js/projects.417a6cb7.js 8.71 KiB 3.00 KiB
|
||||||
|
dist/js/contact-amounts.a32b0ccd.js 8.44 KiB 3.25 KiB
|
||||||
|
dist/js/229.120e09bf.js 7.99 KiB 2.72 KiB
|
||||||
|
dist/js/identity-switcher.c7937333.js 7.44 KiB 2.52 KiB
|
||||||
|
dist/js/new-edit-project.0552181b.js 7.36 KiB 3.11 KiB
|
||||||
|
dist/js/300.dcaeb2a3.js 6.56 KiB 3.24 KiB
|
||||||
|
dist/js/seed-backup.76a0f7b3.js 3.99 KiB 1.97 KiB
|
||||||
|
dist/js/import-derive.c688d4b8.js 3.81 KiB 1.82 KiB
|
||||||
|
dist/js/import-account.c3fa35fd.js 3.54 KiB 1.66 KiB
|
||||||
|
dist/js/new-edit-account.bb763be2.js 3.39 KiB 1.51 KiB
|
||||||
|
dist/js/431.5a6d64e0.js 3.38 KiB 2.56 KiB
|
||||||
|
dist/service-worker.js 3.37 KiB 1.38 KiB
|
||||||
|
dist/js/scan-contact.46be989a.js 2.79 KiB 1.18 KiB
|
||||||
|
dist/js/start.091a7740.js 2.70 KiB 1.30 KiB
|
||||||
|
dist/js/new-identifier.bb379420.js 2.12 KiB 1.18 KiB
|
||||||
|
dist/js/93.b873dbbf.js 2.08 KiB 1.61 KiB
|
||||||
|
dist/js/new-edit-commitment.9248d367.j 1.96 KiB 1.05 KiB
|
||||||
|
s
|
||||||
|
dist/js/confirm-contact.02004d1d.js 1.89 KiB 1.04 KiB
|
||||||
|
dist/js/858.ae4c08ec.js 0.97 KiB 0.78 KiB
|
||||||
|
dist/css/app.8f21529c.css 18.41 KiB 4.39 KiB
|
||||||
|
dist/css/discover.73ee9bd3.css 14.77 KiB 6.25 KiB
|
||||||
|
dist/css/new-edit-project.73ee9bd3.css 14.77 KiB 6.25 KiB
|
||||||
|
dist/css/contacts.abb5e493.css 0.40 KiB 0.23 KiB
|
||||||
|
dist/css/contact-amounts.5b26ccd4.css 0.31 KiB 0.20 KiB
|
||||||
|
dist/css/home.828bc66e.css 0.25 KiB 0.19 KiB
|
||||||
|
dist/css/project.828bc66e.css 0.25 KiB 0.19 KiB
|
||||||
|
dist/css/statistics.828bc66e.css 0.25 KiB 0.19 KiB
|
||||||
|
|
||||||
|
Images and other types of assets omitted.
|
||||||
|
Build at: 2023-09-07T10:06:43.972Z - Hash: 2b39fcd4d0e78263 - Time: 32016ms
|
||||||
|
|
||||||
|
DONE Build complete. The dist directory is ready to be deployed.
|
||||||
|
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
|
||||||
|
|
||||||
319
src/App.vue
319
src/App.vue
@@ -1,5 +1,324 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|
||||||
|
<!-- https://github.com/emmanuelsw/notiwind -->
|
||||||
|
<NotificationGroup group="alert">
|
||||||
|
<div
|
||||||
|
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
|
||||||
|
>
|
||||||
|
<Notification
|
||||||
|
v-slot="{ notifications, close }"
|
||||||
|
enter="transform ease-out duration-300 transition"
|
||||||
|
enter-from="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4"
|
||||||
|
enter-to="translate-y-0 opacity-100 sm:translate-x-0"
|
||||||
|
leave="transition ease-in duration-500"
|
||||||
|
leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0"
|
||||||
|
move="transition duration-500"
|
||||||
|
move-delay="delay-300"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="notification in notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
class="w-full"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'toast'"
|
||||||
|
class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div class="w-full px-4 py-3">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'info'"
|
||||||
|
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-100 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
|
||||||
|
>
|
||||||
|
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
|
||||||
|
>
|
||||||
|
<fa icon="xmark" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'success'"
|
||||||
|
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-emerald-100 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
|
||||||
|
>
|
||||||
|
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
|
||||||
|
>
|
||||||
|
<fa icon="xmark" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'warning'"
|
||||||
|
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-amber-100 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
|
||||||
|
>
|
||||||
|
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
|
||||||
|
>
|
||||||
|
<fa icon="xmark" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'danger'"
|
||||||
|
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-rose-100 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
|
||||||
|
>
|
||||||
|
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
|
||||||
|
>
|
||||||
|
<fa icon="xmark" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Notification>
|
||||||
|
</div>
|
||||||
|
</NotificationGroup>
|
||||||
|
|
||||||
|
<NotificationGroup group="modal">
|
||||||
|
<div class="fixed z-[100] top-0 inset-x-0 w-full">
|
||||||
|
<Notification
|
||||||
|
v-slot="{ notifications, close }"
|
||||||
|
enter="transform ease-out duration-300 transition"
|
||||||
|
enter-from="translate-y-2 opacity-0 sm:translate-y-4"
|
||||||
|
enter-to="translate-y-0 opacity-100 sm:translate-y-0"
|
||||||
|
leave="transition ease-in duration-500"
|
||||||
|
leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0"
|
||||||
|
move="transition duration-500"
|
||||||
|
move-delay="delay-300"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="notification in notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
class="w-full"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'notification-permission'"
|
||||||
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
|
<p class="text-lg mb-4">
|
||||||
|
Would you like to <b>turn on</b> notifications for this app?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Turn on Notifications
|
||||||
|
</button>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Maybe Later
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Never
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'notification-mute'"
|
||||||
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
|
<p class="text-lg mb-4">Mute app notifications:</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
For 1 Hour
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
For 8 Hours
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
For 24 Hours
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Until I turn it back on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'notification-off'"
|
||||||
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
|
<p class="text-lg mb-4">
|
||||||
|
Would you like to <b>turn off</b> notifications for this app?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Turn Off Notifications
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Leave it On
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'pwa-install-gate-ios'"
|
||||||
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
|
<fa
|
||||||
|
icon="mobile-screen-button"
|
||||||
|
class="inline-block text-7xl text-slate-400 mb-4"
|
||||||
|
>
|
||||||
|
</fa>
|
||||||
|
<h3 class="text-2xl font-semibold mb-4">Add to Home Screen</h3>
|
||||||
|
<p class="text-md mb-4">
|
||||||
|
To install the app, you need to add this website to your home
|
||||||
|
screen.
|
||||||
|
</p>
|
||||||
|
<p class="text-md">
|
||||||
|
In your Safari browser menu, tap the
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
<fa
|
||||||
|
icon="arrow-up-from-bracket"
|
||||||
|
class="fa-fw text-slate-500 bg-slate-200 py-1 -my-1 px-0.5 rounded"
|
||||||
|
>
|
||||||
|
</fa>
|
||||||
|
Share
|
||||||
|
</span>
|
||||||
|
icon and choose
|
||||||
|
<b>Add to Home Screen</b> in the options. Then, open the Time
|
||||||
|
Safari app on your home screen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'pwa-install-gate-android'"
|
||||||
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
|
<fa
|
||||||
|
icon="mobile-screen-button"
|
||||||
|
class="inline-block text-7xl text-slate-400 mb-4"
|
||||||
|
>
|
||||||
|
</fa>
|
||||||
|
<h3 class="text-2xl font-semibold mb-4">Add to Home Screen</h3>
|
||||||
|
<p class="text-md mb-4">
|
||||||
|
To install the app, you need to add this website to your home
|
||||||
|
screen.
|
||||||
|
</p>
|
||||||
|
<p class="text-md">
|
||||||
|
In your Chrome browser menu, tap the
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
<fa
|
||||||
|
icon="ellipsis-vertical"
|
||||||
|
class="fa-fw text-slate-500 bg-slate-200 py-1 -my-1 px-0.5 rounded"
|
||||||
|
>
|
||||||
|
</fa>
|
||||||
|
More
|
||||||
|
</span>
|
||||||
|
button and choose
|
||||||
|
<b>Install App</b> in the options.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Notification>
|
||||||
|
</div>
|
||||||
|
</NotificationGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
32
src/components/EntityIcon.vue
Normal file
32
src/components/EntityIcon.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div v-html="generateIdenticon()" class="w-fit"></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
import { toSvg } from "jdenticon";
|
||||||
|
|
||||||
|
const BLANK_CONFIG = {
|
||||||
|
lightness: {
|
||||||
|
color: [1.0, 1.0],
|
||||||
|
grayscale: [1.0, 1.0],
|
||||||
|
},
|
||||||
|
saturation: {
|
||||||
|
color: 0.0,
|
||||||
|
grayscale: 0.0,
|
||||||
|
},
|
||||||
|
backColor: "#0000",
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class EntityIcon extends Vue {
|
||||||
|
@Prop entityId = "";
|
||||||
|
@Prop iconSize = 0;
|
||||||
|
|
||||||
|
generateIdenticon() {
|
||||||
|
const config = this.entityId ? undefined : BLANK_CONFIG;
|
||||||
|
const svgString = toSvg(this.entityId, this.iconSize, config);
|
||||||
|
return svgString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
||||||
129
src/components/GiftedDialog.vue
Normal file
129
src/components/GiftedDialog.vue
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="dialog-overlay">
|
||||||
|
<div class="dialog">
|
||||||
|
<h1 class="text-xl font-bold text-center mb-4">
|
||||||
|
{{ message }} {{ giver?.name || "somebody not specified" }}
|
||||||
|
</h1>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
|
placeholder="What was received"
|
||||||
|
v-model="description"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row mb-6">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2"
|
||||||
|
>Hours</span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="decrement()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
||||||
|
v-model="hours"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="increment()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
|
||||||
|
<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"
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
Sign & Send
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
|
||||||
|
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class GiftedDialog extends Vue {
|
||||||
|
@Prop message = "";
|
||||||
|
|
||||||
|
giver?: GiverInputInfo;
|
||||||
|
description = "";
|
||||||
|
hours = "0";
|
||||||
|
visible = false;
|
||||||
|
|
||||||
|
open(giver: GiverInputInfo) {
|
||||||
|
this.giver = giver;
|
||||||
|
this.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
increment() {
|
||||||
|
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
decrement() {
|
||||||
|
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Emit("dialog-result")
|
||||||
|
confirm(): GiverOutputInfo {
|
||||||
|
const result = {
|
||||||
|
action: "confirm",
|
||||||
|
giver: this.giver,
|
||||||
|
hours: parseFloat(this.hours),
|
||||||
|
description: this.description,
|
||||||
|
};
|
||||||
|
this.close();
|
||||||
|
this.description = "";
|
||||||
|
this.giver = undefined;
|
||||||
|
this.hours = "0";
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Emit("dialog-result")
|
||||||
|
cancel(): GiverOutputInfo {
|
||||||
|
const result = { action: "cancel" };
|
||||||
|
this.close();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background-color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br />
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>vue-cli documentation</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>babel</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>pwa</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>router</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>vuex</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>eslint</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>typescript</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>Forum</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>Community Chat</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
|
|
||||||
>Twitter</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>vue-router</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-devtools#vue-devtools"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>vue-devtools</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>vue-loader</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/awesome-vue"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>awesome-vue</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from "vue-class-component";
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
props: {
|
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class HelloWorld extends Vue {
|
|
||||||
msg!: string;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -10,23 +10,33 @@ import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class InfiniteScroll extends Vue {
|
export default class InfiniteScroll extends Vue {
|
||||||
@Prop({ default: 100 })
|
@Prop({ default: 200 })
|
||||||
readonly distance!: number;
|
readonly distance!: number;
|
||||||
private observer!: IntersectionObserver;
|
private observer!: IntersectionObserver;
|
||||||
|
private isInitialRender = true;
|
||||||
|
|
||||||
mounted() {
|
updated() {
|
||||||
|
if (!this.observer) {
|
||||||
const options = {
|
const options = {
|
||||||
root: this.$refs.scrollContainer as HTMLElement,
|
root: null,
|
||||||
rootMargin: `0px 0px ${this.distance}px 0px`,
|
rootMargin: `0px 0px ${this.distance}px 0px`,
|
||||||
threshold: 1.0,
|
threshold: 1.0,
|
||||||
};
|
};
|
||||||
this.observer = new IntersectionObserver(this.handleIntersection, options);
|
this.observer = new IntersectionObserver(
|
||||||
|
this.handleIntersection,
|
||||||
|
options,
|
||||||
|
);
|
||||||
this.observer.observe(this.$refs.sentinel as HTMLElement);
|
this.observer.observe(this.$refs.sentinel as HTMLElement);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'beforeUnmount' hook runs before unmounting the component
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
|
if (this.observer) {
|
||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Emit("reached-bottom")
|
@Emit("reached-bottom")
|
||||||
handleIntersection(entries: IntersectionObserverEntry[]) {
|
handleIntersection(entries: IntersectionObserverEntry[]) {
|
||||||
const entry = entries[0];
|
const entry = entries[0];
|
||||||
|
|||||||
93
src/components/QuickNav.vue
Normal file
93
src/components/QuickNav.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<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': true,
|
||||||
|
'rounded-md': true,
|
||||||
|
'bg-slate-400 text-white': selected === 'Home',
|
||||||
|
'text-slate-500': selected !== 'Home',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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': true,
|
||||||
|
'rounded-md': true,
|
||||||
|
'bg-slate-400 text-white': selected === 'Discover',
|
||||||
|
'text-slate-500': selected !== 'Discover',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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': true,
|
||||||
|
'rounded-md': true,
|
||||||
|
'bg-slate-400 text-white': selected === 'Projects',
|
||||||
|
'text-slate-500': selected !== 'Projects',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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': true,
|
||||||
|
'rounded-md': true,
|
||||||
|
'bg-slate-400 text-white': selected === 'Contacts',
|
||||||
|
'text-slate-500': selected !== 'Contacts',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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': true,
|
||||||
|
'rounded-md': true,
|
||||||
|
'bg-slate-400 text-white': selected === 'Profile',
|
||||||
|
'text-slate-500': selected !== 'Profile',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class QuickNav extends Vue {
|
||||||
|
@Prop selected = "";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -5,7 +5,7 @@ function createCamera() {
|
|||||||
35, // fov = Field Of View
|
35, // fov = Field Of View
|
||||||
1, // aspect ratio (dummy value)
|
1, // aspect ratio (dummy value)
|
||||||
0.1, // near clipping plane
|
0.1, // near clipping plane
|
||||||
350 // far clipping plane
|
350, // far clipping plane
|
||||||
);
|
);
|
||||||
|
|
||||||
// move the camera back so we can view the scene
|
// move the camera back so we can view the scene
|
||||||
|
|||||||
@@ -22,14 +22,16 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
|
||||||
const token = await accessToken(identity);
|
|
||||||
|
|
||||||
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
};
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
if (identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
headers["Authorization"] = "Bearer " + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
|
||||||
const resp = await axios.get(url, { headers: headers });
|
const resp = await axios.get(url, { headers: headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const landmarks = resp.data.data;
|
const landmarks = resp.data.data;
|
||||||
@@ -60,7 +62,11 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
|
|
||||||
// calculate positions for each claim, especially because some are random
|
// calculate positions for each claim, especially because some are random
|
||||||
const locations = landmarks.map((claim) =>
|
const locations = landmarks.map((claim) =>
|
||||||
locForGive(claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS)
|
locForGive(
|
||||||
|
claim,
|
||||||
|
world.PLATFORM_SIZE,
|
||||||
|
world.PLATFORM_EDGE_FOR_UNKNOWNS,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
@@ -90,7 +96,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
undefined,
|
undefined,
|
||||||
function (error) {
|
function (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// calculate when lights shine on appearing claim area
|
// calculate when lights shine on appearing claim area
|
||||||
@@ -118,7 +124,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
.onComplete(() => {
|
.onComplete(() => {
|
||||||
scene.remove(light);
|
scene.remove(light);
|
||||||
light.dispose();
|
light.dispose();
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.start();
|
.start();
|
||||||
world.lights = [...world.lights, light];
|
world.lights = [...world.lights, light];
|
||||||
@@ -127,18 +133,18 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
console.error(
|
console.error(
|
||||||
"Got bad server response status & data of",
|
"Got bad server response status & data of",
|
||||||
resp.status,
|
resp.status,
|
||||||
resp.data
|
resp.data,
|
||||||
);
|
);
|
||||||
vue.setAlert(
|
vue.setAlert(
|
||||||
"Error With Server",
|
"Error With Server",
|
||||||
"There was an error retrieving your claims from the server."
|
"There was an error retrieving your claims from the server.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Got exception contacting server:", error);
|
console.error("Got exception contacting server:", error);
|
||||||
vue.setAlert(
|
vue.setAlert(
|
||||||
"Error With Server",
|
"Error With Server",
|
||||||
"There was a problem retrieving your claims from the server."
|
"There was a problem retrieving your claims from the server.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
|
|||||||
|
|
||||||
export function createTerrain(props) {
|
export function createTerrain(props) {
|
||||||
const loader = new TextureLoader();
|
const loader = new TextureLoader();
|
||||||
const height = loader.load("img/textures/forest-floor.png");
|
const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg");
|
||||||
// w h
|
// w h
|
||||||
const geometry = new PlaneGeometry(props.width, props.height, 64, 64);
|
const geometry = new PlaneGeometry(props.width, props.height, 64, 64);
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
export enum AppString {
|
export enum AppString {
|
||||||
APP_NAME = "Kick-Start with Time",
|
APP_NAME = "Kick-Start with Time",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://endorser.ch:3000",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||||
|
|
||||||
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ type NonsensitiveTables = {
|
|||||||
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
|
* 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 type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||||
export const accountsDB = new BaseDexie("KickStartAccounts") as SensitiveDexie;
|
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||||
const SensitiveSchemas = Object.assign({}, AccountsSchema);
|
const SensitiveSchemas = Object.assign({}, AccountsSchema);
|
||||||
|
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||||
BaseDexie & T;
|
BaseDexie & T;
|
||||||
export const db = new BaseDexie("KickStart") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,12 @@ const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
|||||||
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
|
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// create password and place password in localStorage
|
/**
|
||||||
|
* Create password and place password in localStorage.
|
||||||
|
*
|
||||||
|
* It's good practice to keep the data encrypted at rest, so we'll do that even
|
||||||
|
* if the secret is stored right next to the app.
|
||||||
|
*/
|
||||||
const secret =
|
const secret =
|
||||||
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
||||||
|
|
||||||
@@ -49,7 +54,6 @@ if (localStorage.getItem("secret") == null) {
|
|||||||
localStorage.setItem("secret", secret);
|
localStorage.setItem("secret", secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("IndexedDB Encryption Secret:", secret);
|
|
||||||
encrypted(accountsDB, { secretKey: secret });
|
encrypted(accountsDB, { secretKey: secret });
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Represents an account stored in the database.
|
||||||
|
*/
|
||||||
export type Account = {
|
export type Account = {
|
||||||
id?: number; // auto-generated by Dexie
|
/**
|
||||||
|
* Auto-generated ID by Dexie.
|
||||||
|
*/
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The date the account was created.
|
||||||
|
*/
|
||||||
dateCreated: string;
|
dateCreated: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The derivation path for the account.
|
||||||
|
*/
|
||||||
derivationPath: string;
|
derivationPath: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decentralized Identifier (DID) for the account.
|
||||||
|
*/
|
||||||
did: string;
|
did: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stringified JSON containing underlying key material.
|
||||||
|
* Based on the IIdentifier type from Veramo.
|
||||||
|
* @see {@link https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts}
|
||||||
|
*/
|
||||||
identity: string;
|
identity: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The public key in hexadecimal format.
|
||||||
|
*/
|
||||||
publicKeyHex: string;
|
publicKeyHex: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mnemonic passphrase for the account.
|
||||||
|
*/
|
||||||
mnemonic: 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
|
* Schema for the accounts table in the database.
|
||||||
|
* Fields starting with a $ character are encrypted.
|
||||||
|
* @see {@link https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon}
|
||||||
|
*/
|
||||||
export const AccountsSchema = {
|
export const AccountsSchema = {
|
||||||
accounts:
|
accounts:
|
||||||
"++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",
|
"++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
|
export type BoundingBox = {
|
||||||
|
eastLong: number;
|
||||||
|
maxLat: number;
|
||||||
|
minLat: number;
|
||||||
|
westLong: number;
|
||||||
|
};
|
||||||
|
|
||||||
// a singleton
|
// a singleton
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
id: number; // there's only one entry: MASTER_SETTINGS_KEY
|
id: number; // there's only one entry: MASTER_SETTINGS_KEY
|
||||||
|
|
||||||
activeDid?: string;
|
activeDid?: string;
|
||||||
apiServer?: string;
|
apiServer?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
lastViewedClaimId?: string;
|
||||||
|
searchBoxes?: Array<{
|
||||||
|
name: string;
|
||||||
|
bbox: BoundingBox;
|
||||||
|
}>;
|
||||||
showContactGivesInline?: boolean;
|
showContactGivesInline?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { HDNode } from "@ethersproject/hdnode";
|
|||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import * as u8a from "uint8arrays";
|
import * as u8a from "uint8arrays";
|
||||||
|
|
||||||
|
export const DEFAULT_ROOT_DERIVATION_PATH = "m/76798669'/0'/0'/0'";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
@@ -20,7 +22,7 @@ export const newIdentifier = (
|
|||||||
address: string,
|
address: string,
|
||||||
publicHex: string,
|
publicHex: string,
|
||||||
privateHex: string,
|
privateHex: string,
|
||||||
derivationPath: string
|
derivationPath: string,
|
||||||
): Omit<IIdentifier, keyof "provider"> => {
|
): Omit<IIdentifier, keyof "provider"> => {
|
||||||
return {
|
return {
|
||||||
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
||||||
@@ -46,18 +48,18 @@ export const newIdentifier = (
|
|||||||
* @return {*} {[string, string, string, string]}
|
* @return {*} {[string, string, string, string]}
|
||||||
*/
|
*/
|
||||||
export const deriveAddress = (
|
export const deriveAddress = (
|
||||||
mnemonic: string
|
mnemonic: string,
|
||||||
|
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
): [string, string, string, string] => {
|
): [string, string, string, string] => {
|
||||||
const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
|
|
||||||
mnemonic = mnemonic.trim().toLowerCase();
|
mnemonic = mnemonic.trim().toLowerCase();
|
||||||
|
|
||||||
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
|
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
|
||||||
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH);
|
const rootNode: HDNode = hdnode.derivePath(derivationPath);
|
||||||
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
|
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
|
||||||
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
|
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
|
||||||
const address = rootNode.address;
|
const address = rootNode.address;
|
||||||
|
|
||||||
return [address, privateHex, publicHex, UPORT_ROOT_DERIVATION_PATH];
|
return [address, privateHex, publicHex, derivationPath];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,7 +136,7 @@ export function fromJose(signature: string): {
|
|||||||
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
|
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
|
||||||
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`
|
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const r = bytesToHex(signatureBytes.slice(0, 32));
|
const r = bytesToHex(signatureBytes.slice(0, 32));
|
||||||
|
|||||||
@@ -1,6 +1,38 @@
|
|||||||
|
import * as R from "ramda";
|
||||||
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||||
|
import * as didJwt from "did-jwt";
|
||||||
|
import { Axios, AxiosResponse } from "axios";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||||
export const SERVICE_ID = "endorser.ch";
|
export const SERVICE_ID = "endorser.ch";
|
||||||
|
|
||||||
|
export interface AgreeVerifiableCredential {
|
||||||
|
"@context": string;
|
||||||
|
"@type": string;
|
||||||
|
// "any" because arbitrary objects can be subject of agreement
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
object: Record<any, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GiverInputInfo {
|
||||||
|
did?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GiverOutputInfo {
|
||||||
|
action: string;
|
||||||
|
giver?: GiverInputInfo;
|
||||||
|
description?: string;
|
||||||
|
hours?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaimResult {
|
||||||
|
success: { claimId: string; handleId: string };
|
||||||
|
error: { code: string; message: string };
|
||||||
|
}
|
||||||
|
|
||||||
export interface GenericClaim {
|
export interface GenericClaim {
|
||||||
"@context": string;
|
"@context": string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
@@ -10,14 +42,6 @@ export interface GenericClaim {
|
|||||||
claim: Record<any, any>;
|
claim: Record<any, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgreeVerifiableCredential {
|
|
||||||
"@context": string;
|
|
||||||
"@type": string;
|
|
||||||
// "any" because arbitrary objects can be subject of agreement
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
object: Record<any, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GiveServerRecord {
|
export interface GiveServerRecord {
|
||||||
agentDid: string;
|
agentDid: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -33,11 +57,35 @@ export interface GiveServerRecord {
|
|||||||
export interface GiveVerifiableCredential {
|
export interface GiveVerifiableCredential {
|
||||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||||
"@type": string;
|
"@type": string;
|
||||||
agent: { identifier: string };
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
|
fulfills?: { "@type": string; identifier: string };
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
object: { amountOfThisGood: number; unitCode: string };
|
object?: { amountOfThisGood: number; unitCode: string };
|
||||||
recipient: { identifier: string };
|
recipient?: { identifier: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanVerifiableCredential {
|
||||||
|
"@context": "https://schema.org";
|
||||||
|
"@type": "PlanAction";
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
identifier?: string;
|
||||||
|
location?: {
|
||||||
|
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanServerRecord {
|
||||||
|
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
||||||
|
description: string;
|
||||||
|
endTime?: string;
|
||||||
|
issuerDid: string;
|
||||||
|
handleId: string;
|
||||||
|
locLat?: number;
|
||||||
|
locLon?: number;
|
||||||
|
startTime?: string;
|
||||||
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterVerifiableCredential {
|
export interface RegisterVerifiableCredential {
|
||||||
@@ -45,5 +93,208 @@ export interface RegisterVerifiableCredential {
|
|||||||
"@type": string;
|
"@type": string;
|
||||||
agent: { identifier: string };
|
agent: { identifier: string };
|
||||||
object: string;
|
object: string;
|
||||||
recipient: { identifier: string };
|
participant: { identifier: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InternalError {
|
||||||
|
error: string; // for system logging
|
||||||
|
userMessage?: string; // for user display
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used to check for hidden info.
|
||||||
|
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||||
|
const HIDDEN_DID = "did:none:HIDDEN";
|
||||||
|
|
||||||
|
export function isHiddenDid(did: string) {
|
||||||
|
return did === HIDDEN_DID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
||||||
|
**/
|
||||||
|
export function didInfo(
|
||||||
|
did: string,
|
||||||
|
activeDid: string,
|
||||||
|
allMyDids: string[],
|
||||||
|
contacts: Contact[],
|
||||||
|
): string {
|
||||||
|
const myId = R.find(R.equals(did), allMyDids);
|
||||||
|
if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`;
|
||||||
|
|
||||||
|
const contact = R.find((c) => c.did === did, contacts);
|
||||||
|
return contact
|
||||||
|
? contact.name || "Someone Unnamed in Contacts"
|
||||||
|
: !did
|
||||||
|
? "Unspecified Person"
|
||||||
|
: isHiddenDid(did)
|
||||||
|
? "Someone Not In Network"
|
||||||
|
: "Someone Not In Contacts";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResultWithType {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuccessResult extends ResultWithType {
|
||||||
|
type: "success";
|
||||||
|
response: AxiosResponse<ClaimResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResult {
|
||||||
|
type: "error";
|
||||||
|
error: InternalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
|
*
|
||||||
|
* @param identity
|
||||||
|
* @param fromDid may be null
|
||||||
|
* @param toDid
|
||||||
|
* @param description may be null; should have this or hours
|
||||||
|
* @param hours may be null; should have this or description
|
||||||
|
*/
|
||||||
|
export async function createAndSubmitGive(
|
||||||
|
axios: Axios,
|
||||||
|
apiServer: string,
|
||||||
|
identity: IIdentifier,
|
||||||
|
fromDid?: string,
|
||||||
|
toDid?: string,
|
||||||
|
description?: string,
|
||||||
|
hours?: number,
|
||||||
|
fulfillsProjectHandleId?: string,
|
||||||
|
): Promise<CreateAndSubmitGiveResult> {
|
||||||
|
try {
|
||||||
|
const vcClaim: GiveVerifiableCredential = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "GiveAction",
|
||||||
|
recipient: toDid ? { identifier: toDid } : undefined,
|
||||||
|
agent: fromDid ? { identifier: fromDid } : undefined,
|
||||||
|
description: description || undefined,
|
||||||
|
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
|
||||||
|
fulfills: fulfillsProjectHandleId
|
||||||
|
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const vcPayload = {
|
||||||
|
vc: {
|
||||||
|
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||||
|
type: ["VerifiableCredential"],
|
||||||
|
credentialSubject: vcClaim,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a signature using private key of identity
|
||||||
|
const firstKey = identity.keys[0];
|
||||||
|
const privateKeyHex = firstKey?.privateKeyHex;
|
||||||
|
|
||||||
|
if (!privateKeyHex) {
|
||||||
|
throw {
|
||||||
|
error: "No private key",
|
||||||
|
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const signer = await SimpleSigner(privateKeyHex);
|
||||||
|
|
||||||
|
// Create a JWT for the request
|
||||||
|
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||||
|
issuer: identity.did,
|
||||||
|
signer,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make the xhr request payload
|
||||||
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||||
|
const url = `${apiServer}/api/v2/claim`;
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
|
||||||
|
const response = await axios.post(url, payload, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { type: "success", response };
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage: string =
|
||||||
|
error === null
|
||||||
|
? "Null error"
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: typeof error === "object" && error !== null && "message" in error
|
||||||
|
? (error as { message: string }).message
|
||||||
|
: "Unknown error";
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
error: {
|
||||||
|
error: errorMessage,
|
||||||
|
userMessage: "Failed to create and submit the claim.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// from https://stackoverflow.com/a/175787/845494
|
||||||
|
//
|
||||||
|
export function isNumeric(str: string): boolean {
|
||||||
|
return !isNaN(+str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numberOrZero(str: string): number {
|
||||||
|
return isNumeric(str) ? +str : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error?: {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimits {
|
||||||
|
doneClaimsThisWeek: string;
|
||||||
|
doneRegistrationsThisMonth: string;
|
||||||
|
maxClaimsPerWeek: string;
|
||||||
|
maxRegistrationsPerMonth: string;
|
||||||
|
nextMonthBeginDateTime: string;
|
||||||
|
nextWeekBeginDateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents data about a project
|
||||||
|
**/
|
||||||
|
export interface ProjectData {
|
||||||
|
/**
|
||||||
|
* Name of the project
|
||||||
|
**/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Description of the project
|
||||||
|
**/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* URL referencing information about the project
|
||||||
|
**/
|
||||||
|
handleId: string;
|
||||||
|
/**
|
||||||
|
* The Identier of the project
|
||||||
|
**/
|
||||||
|
rowid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifiableCredential {
|
||||||
|
"@context": string;
|
||||||
|
"@type": string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
identifier?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorldProperties {
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function didProviderName(netName: string) {
|
|||||||
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
|
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
|
||||||
|
|
||||||
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
|
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
|
||||||
DEFAULT_DID_PROVIDER_NETWORK_NAME
|
DEFAULT_DID_PROVIDER_NETWORK_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const HANDY_APP = false;
|
export const HANDY_APP = false;
|
||||||
|
|||||||
34
src/main.ts
34
src/main.ts
@@ -5,17 +5,27 @@ import "./registerServiceWorker";
|
|||||||
import router from "./router";
|
import router from "./router";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import VueAxios from "vue-axios";
|
import VueAxios from "vue-axios";
|
||||||
|
import Notifications from "notiwind";
|
||||||
|
|
||||||
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 {
|
||||||
|
faArrowLeft,
|
||||||
|
faArrowRight,
|
||||||
|
faArrowUpFromBracket,
|
||||||
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
faChevronRight,
|
||||||
faCircle,
|
faCircle,
|
||||||
faCircleCheck,
|
faCircleCheck,
|
||||||
|
faCircleInfo,
|
||||||
faCircleQuestion,
|
faCircleQuestion,
|
||||||
faCircleUser,
|
faCircleUser,
|
||||||
|
faClock,
|
||||||
|
faCoins,
|
||||||
|
faComment,
|
||||||
faCopy,
|
faCopy,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -23,11 +33,14 @@ import {
|
|||||||
faFileLines,
|
faFileLines,
|
||||||
faFloppyDisk,
|
faFloppyDisk,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
|
faGift,
|
||||||
faHand,
|
faHand,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
|
faLocationDot,
|
||||||
faLongArrowAltLeft,
|
faLongArrowAltLeft,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
|
faMobileScreenButton,
|
||||||
faPen,
|
faPen,
|
||||||
faPersonCircleCheck,
|
faPersonCircleCheck,
|
||||||
faPersonCircleQuestion,
|
faPersonCircleQuestion,
|
||||||
@@ -36,19 +49,31 @@ import {
|
|||||||
faRotate,
|
faRotate,
|
||||||
faShareNodes,
|
faShareNodes,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
|
faSquareCaretDown,
|
||||||
|
faSquareCaretUp,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
faXmark,
|
faXmark,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
faArrowLeft,
|
||||||
|
faArrowRight,
|
||||||
|
faArrowUpFromBracket,
|
||||||
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
faChevronRight,
|
||||||
faCircle,
|
faCircle,
|
||||||
faCircleCheck,
|
faCircleCheck,
|
||||||
|
faCircleInfo,
|
||||||
faCircleQuestion,
|
faCircleQuestion,
|
||||||
faCircleUser,
|
faCircleUser,
|
||||||
|
faClock,
|
||||||
|
faCoins,
|
||||||
|
faComment,
|
||||||
faCopy,
|
faCopy,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -56,11 +81,14 @@ library.add(
|
|||||||
faFileLines,
|
faFileLines,
|
||||||
faFloppyDisk,
|
faFloppyDisk,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
|
faGift,
|
||||||
faHand,
|
faHand,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
|
faLocationDot,
|
||||||
faLongArrowAltLeft,
|
faLongArrowAltLeft,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
|
faMobileScreenButton,
|
||||||
faPen,
|
faPen,
|
||||||
faPersonCircleCheck,
|
faPersonCircleCheck,
|
||||||
faPersonCircleQuestion,
|
faPersonCircleQuestion,
|
||||||
@@ -69,10 +97,13 @@ library.add(
|
|||||||
faRotate,
|
faRotate,
|
||||||
faShareNodes,
|
faShareNodes,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
|
faSquareCaretDown,
|
||||||
|
faSquareCaretUp,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
faXmark
|
faXmark,
|
||||||
);
|
);
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
@@ -82,4 +113,5 @@ createApp(App)
|
|||||||
.use(createPinia())
|
.use(createPinia())
|
||||||
.use(VueAxios, axios)
|
.use(VueAxios, axios)
|
||||||
.use(router)
|
.use(router)
|
||||||
|
.use(Notifications)
|
||||||
.mount("#app");
|
.mount("#app");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
ready() {
|
ready() {
|
||||||
console.log(
|
console.log(
|
||||||
"App is being served from cache by a service worker.\n" +
|
"App is being served from cache by a service worker.\n" +
|
||||||
"For more details, visit https://goo.gl/AFskqB"
|
"For more details, visit https://goo.gl/AFskqB",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
registered() {
|
registered() {
|
||||||
@@ -24,7 +24,7 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
},
|
},
|
||||||
offline() {
|
offline() {
|
||||||
console.log(
|
console.log(
|
||||||
"No internet connection found. App is running in offline mode."
|
"No internet connection found. App is running in offline mode.",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
error(error) {
|
error(error) {
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
import {
|
||||||
import { accountsDB } from "@/db";
|
createRouter,
|
||||||
|
createWebHistory,
|
||||||
|
NavigationGuardNext,
|
||||||
|
RouteLocationNormalized,
|
||||||
|
RouteRecordRaw,
|
||||||
|
} from "vue-router";
|
||||||
|
import { accountsDB } from "@/db/index";
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
/**
|
||||||
{
|
*
|
||||||
path: "/",
|
* @param to :RouteLocationNormalized
|
||||||
name: "home",
|
* @param from :RouteLocationNormalized
|
||||||
component: () =>
|
* @param next :NavigationGuardNext
|
||||||
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"),
|
*/
|
||||||
beforeEnter: async (to, from, next) => {
|
const enterOrStart = async (
|
||||||
|
to: RouteLocationNormalized,
|
||||||
|
from: RouteLocationNormalized,
|
||||||
|
next: NavigationGuardNext,
|
||||||
|
) => {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
const num_accounts = await accountsDB.accounts.count();
|
||||||
if (num_accounts > 0) {
|
if (num_accounts > 0) {
|
||||||
@@ -15,19 +25,22 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
} else {
|
} else {
|
||||||
next({ name: "start" });
|
next({ name: "start" });
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
},
|
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
path: "/about",
|
path: "/",
|
||||||
name: "about",
|
name: "home",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
|
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
|
||||||
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/account",
|
path: "/account",
|
||||||
name: "account",
|
name: "account",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
|
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
|
||||||
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/confirm-contact",
|
path: "/confirm-contact",
|
||||||
@@ -58,6 +71,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "contacts",
|
name: "contacts",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
|
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
|
||||||
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/scan-contact",
|
path: "/scan-contact",
|
||||||
@@ -87,6 +101,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "import-account" */ "../views/ImportAccountView.vue"
|
/* webpackChunkName: "import-account" */ "../views/ImportAccountView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/import-derive",
|
||||||
|
name: "import-derive",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "import-derive" */ "../views/ImportDerivedAccountView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/new-edit-account",
|
path: "/new-edit-account",
|
||||||
name: "new-edit-account",
|
name: "new-edit-account",
|
||||||
@@ -111,6 +133,22 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
|
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/new-identifier",
|
||||||
|
name: "new-identifier",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/identity-switcher",
|
||||||
|
name: "identity-switcher",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/project",
|
path: "/project",
|
||||||
name: "project",
|
name: "project",
|
||||||
@@ -122,6 +160,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "projects",
|
name: "projects",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
|
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
|
||||||
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/seed-backup",
|
path: "/seed-backup",
|
||||||
@@ -145,6 +184,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
|
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/contact-gives",
|
||||||
|
name: "contact-gives",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @type {*} */
|
/** @type {*} */
|
||||||
@@ -153,4 +200,18 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const errorHandler = (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
error: any,
|
||||||
|
to: RouteLocationNormalized,
|
||||||
|
from: RouteLocationNormalized,
|
||||||
|
) => {
|
||||||
|
// Handle the error here
|
||||||
|
console.error("Caught in top level error handler:", error, to, from);
|
||||||
|
|
||||||
|
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
||||||
|
};
|
||||||
|
|
||||||
|
router.onError(errorHandler); // Assign the error handler to the router instance
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from "vue-class-component";
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
components: {},
|
|
||||||
})
|
|
||||||
export default class AboutView extends Vue {}
|
|
||||||
</script>
|
|
||||||
@@ -1,59 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
|
||||||
<ul class="flex text-2xl p-2 gap-2">
|
|
||||||
<!-- Home Feed -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
|
|
||||||
<fa icon="house-chimney" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Search -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'discover' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Projects -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'projects' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="folder-open" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Contacts -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contacts' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="users" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Profile -->
|
|
||||||
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="circle-user" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
||||||
Your Identity
|
Your Identity
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span />
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'contact-qr' }"
|
||||||
|
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="qrcode" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-2">
|
<div class="flex justify-between py-2">
|
||||||
<span />
|
<span />
|
||||||
<span>
|
<span>
|
||||||
@@ -66,20 +32,22 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Friend referral requirement notice -->
|
<!-- Registration notice -->
|
||||||
|
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
|
||||||
<div
|
<div
|
||||||
|
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
|
||||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
<b>Important:</b> before you can create a new project or commit time to
|
<b>Note:</b> Before you can publicly announce a new project or time
|
||||||
one, you need a friend to register you.
|
commitment, a friend needs to register you.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<router-link
|
||||||
id="btnShowQR"
|
:to="{ name: 'contact-qr' }"
|
||||||
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Share Your ID
|
Share Your Info
|
||||||
</button>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Identity Details -->
|
<!-- Identity Details -->
|
||||||
@@ -98,14 +66,6 @@
|
|||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
<span v-show="showDidCopy">Copied!</span>
|
<span v-show="showDidCopy">Copied!</span>
|
||||||
<span class="whitespace-nowrap ml-4">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contact-qr' }"
|
|
||||||
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md ml-1"
|
|
||||||
>
|
|
||||||
<fa icon="qrcode" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</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 (base 64)</div>
|
||||||
@@ -153,11 +113,72 @@
|
|||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'new-edit-account' }"
|
:to="{ name: 'new-edit-account' }"
|
||||||
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-2"
|
||||||
>
|
>
|
||||||
Edit Identity
|
Edit Identity
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||||
|
<label
|
||||||
|
for="toggleNotifications"
|
||||||
|
class="flex items-center cursor-pointer"
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'modal',
|
||||||
|
type: 'notification-permission',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- toggle -->
|
||||||
|
<div class="relative">
|
||||||
|
<!-- input -->
|
||||||
|
<input type="checkbox" name="toggleNotifications" 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">App Notifications</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
for="toggleMuteNotifications"
|
||||||
|
class="flex items-center cursor-pointer mt-4"
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'modal',
|
||||||
|
type: 'notification-mute',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- toggle -->
|
||||||
|
<div class="relative">
|
||||||
|
<!-- input -->
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="toggleMuteNotifications"
|
||||||
|
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">Mute Notifications</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
@@ -177,11 +198,10 @@
|
|||||||
|
|
||||||
<!-- 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">
|
||||||
<form method="dialog">
|
|
||||||
<div class="text-slate-500 text-center">
|
<div class="text-slate-500 text-center">
|
||||||
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
||||||
</div>
|
</div>
|
||||||
<img src="img/sample-qr-code.png" class="w-full mb-3" />
|
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
value="cancel"
|
value="cancel"
|
||||||
@@ -195,11 +215,42 @@
|
|||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Advanced</h3>
|
<div class="flex py-2">
|
||||||
|
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
||||||
|
Check Limits
|
||||||
|
</button>
|
||||||
|
<!-- show spinner if loading limits -->
|
||||||
|
<div v-if="loadingLimits" class="ml-2">
|
||||||
|
Checking... <fa icon="spinner" class="fa-spin"></fa>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2">
|
||||||
|
{{ limitsMessage }}
|
||||||
|
</div>
|
||||||
|
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<h3
|
||||||
|
class="text-sm uppercase font-semibold mb-3"
|
||||||
|
@click="showAdvanced = !showAdvanced"
|
||||||
|
>
|
||||||
|
Advanced
|
||||||
|
</h3>
|
||||||
|
<div v-if="showAdvanced">
|
||||||
<label
|
<label
|
||||||
for="toggleShowAmounts"
|
for="toggleShowAmounts"
|
||||||
class="flex items-center cursor-pointer mb-6"
|
class="flex items-center cursor-pointer mb-6"
|
||||||
@@ -226,23 +277,12 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
<router-link
|
||||||
Check Limits
|
:to="{ name: 'identity-switcher' }"
|
||||||
</button>
|
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
>
|
||||||
<span class="font-bold">Rate Limits</span>
|
Switch Identity / No Identity
|
||||||
<p>
|
</router-link>
|
||||||
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>
|
||||||
|
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
@@ -254,10 +294,10 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="apiServerInput != apiServer"
|
v-if="apiServerInput != apiServer"
|
||||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
class="px-4 rounded bg-red-500 border border-slate-400"
|
||||||
@click="onClickSaveApiServer()"
|
@click="onClickSaveApiServer()"
|
||||||
>
|
>
|
||||||
<fa icon="floppy-disk" class="fa-fw"></fa>
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||||
@@ -279,72 +319,63 @@
|
|||||||
</button>
|
</button>
|
||||||
</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>
|
<div>
|
||||||
<button class="text-blue-500 px-2">
|
<button class="text-blue-500">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'statistics' }"
|
:to="{ name: 'statistics' }"
|
||||||
class="block text-center py-3 px-1"
|
class="block text-center py-3"
|
||||||
>
|
>
|
||||||
See Achievements & Statistics
|
See Achievements & Statistics
|
||||||
</router-link>
|
</router-link>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- This same popup code is in many files. -->
|
|
||||||
<div v-bind:class="computedAlertClassNames()">
|
|
||||||
<button
|
|
||||||
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
|
||||||
@click="onClickClose()"
|
|
||||||
>
|
|
||||||
<fa icon="xmark"></fa>
|
|
||||||
</button>
|
|
||||||
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
|
||||||
<p>{{ alertMessage }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { AxiosError } from "axios";
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
import * as R from "ramda";
|
|
||||||
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import {
|
import { accessToken } from "@/libs/crypto";
|
||||||
accessToken,
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
deriveAddress,
|
import { IIdentifier } from "@veramo/core";
|
||||||
generateSeed,
|
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
||||||
newIdentifier,
|
|
||||||
} from "@/libs/crypto";
|
|
||||||
import { AxiosError } from "axios/index";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
|
||||||
interface RateLimits {
|
interface Notification {
|
||||||
doneClaimsThisWeek: string;
|
group: string;
|
||||||
doneRegistrationsThisMonth: string;
|
type: string;
|
||||||
maxClaimsPerWeek: string;
|
title: string;
|
||||||
maxRegistrationsPerMonth: string;
|
text: string;
|
||||||
nextMonthBeginDateTime: string;
|
|
||||||
nextWeekBeginDateTime: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component
|
interface IAccount {
|
||||||
|
did: string;
|
||||||
|
publicKeyHex: string;
|
||||||
|
privateHex?: string;
|
||||||
|
derivationPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsType {
|
||||||
|
activeDid?: string;
|
||||||
|
apiServer?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
showContactGivesInline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({ components: { QuickNav } })
|
||||||
export default class AccountViewView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
Constants = AppString;
|
Constants = AppString;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
@@ -357,6 +388,8 @@ export default class AccountViewView extends Vue {
|
|||||||
publicHex = "";
|
publicHex = "";
|
||||||
publicBase64 = "";
|
publicBase64 = "";
|
||||||
limits: RateLimits | null = null;
|
limits: RateLimits | null = null;
|
||||||
|
limitsMessage = "";
|
||||||
|
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
|
||||||
showContactGives = false;
|
showContactGives = false;
|
||||||
|
|
||||||
showDidCopy = false;
|
showDidCopy = false;
|
||||||
@@ -364,8 +397,64 @@ export default class AccountViewView extends Vue {
|
|||||||
showB64Copy = false;
|
showB64Copy = false;
|
||||||
showPubCopy = false;
|
showPubCopy = false;
|
||||||
|
|
||||||
|
showAdvanced = false;
|
||||||
|
alertMessage = "";
|
||||||
|
alertTitle = "";
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
||||||
|
try {
|
||||||
|
// Open the accounts database
|
||||||
|
await accountsDB.open();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open accounts database:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let account: { identity?: string } | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Search for the account with the matching DID (decentralized identifier)
|
||||||
|
account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to find account:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return parsed identity or null if not found
|
||||||
|
return JSON.parse(account?.identity || "null");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously retrieves headers for HTTP requests.
|
||||||
|
*
|
||||||
|
* @param {IIdentifier} identity - The identity object for which to generate the headers.
|
||||||
|
* @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
|
||||||
|
*
|
||||||
|
* @throws Will throw an error if unable to generate an access token.
|
||||||
|
*/
|
||||||
|
public async getHeaders(
|
||||||
|
identity: IIdentifier,
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get headers:", error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
||||||
doCopyTwoSecRedo(text, fn) {
|
doCopyTwoSecRedo(text: string, fn: () => void) {
|
||||||
fn();
|
fn();
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(text)
|
.copy(text)
|
||||||
@@ -381,52 +470,61 @@ export default class AccountViewView extends Vue {
|
|||||||
return timeStr.substring(0, timeStr.indexOf("T"));
|
return timeStr.substring(0, timeStr.indexOf("T"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async function executed when the component is created.
|
||||||
|
* Initializes the component's state with values from the database,
|
||||||
|
* handles identity-related tasks, and checks limitations.
|
||||||
|
*
|
||||||
|
* @throws Will display specific messages to the user based on different errors.
|
||||||
|
*/
|
||||||
async 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 {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
|
||||||
|
// Initialize component state with values from the database or defaults
|
||||||
|
this.initializeState(settings);
|
||||||
|
|
||||||
|
// Get and process the identity
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
if (identity) {
|
||||||
|
this.processIdentity(identity);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.handleError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes component state with values from the database or defaults.
|
||||||
|
* @param {SettingsType} settings - Object containing settings from the database.
|
||||||
|
*/
|
||||||
|
initializeState(settings: SettingsType | undefined) {
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.apiServerInput = settings?.apiServer || "";
|
this.apiServerInput = settings?.apiServer || "";
|
||||||
this.firstName = settings?.firstName || "";
|
this.firstName = settings?.firstName || "";
|
||||||
this.lastName = settings?.lastName || "";
|
this.lastName = settings?.lastName || "";
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
await accountsDB.open();
|
|
||||||
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);
|
* Processes the identity and updates the component's state.
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
* @param {IdentityType} identity - Object containing identity information.
|
||||||
|
*/
|
||||||
|
processIdentity(identity: IIdentifier) {
|
||||||
|
if (
|
||||||
|
identity &&
|
||||||
|
identity.keys &&
|
||||||
|
identity.keys.length > 0 &&
|
||||||
|
identity.keys[0].meta
|
||||||
|
) {
|
||||||
this.publicHex = identity.keys[0].publicKeyHex;
|
this.publicHex = identity.keys[0].publicKeyHex;
|
||||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||||
this.derivationPath = identity.keys[0].meta.derivationPath;
|
this.derivationPath = identity.keys[0].meta.derivationPath;
|
||||||
@@ -434,12 +532,35 @@ export default class AccountViewView extends Vue {
|
|||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
activeDid: identity.did,
|
activeDid: identity.did,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
this.checkLimitsFor(identity);
|
||||||
this.alertMessage =
|
} else {
|
||||||
"Clear your cache and start over (after data backup). See console log for more info.";
|
// Handle the case where any of these are null or undefined
|
||||||
console.error("Telling user to clear cache because:", err);
|
}
|
||||||
this.alertTitle = "Error Creating Account";
|
}
|
||||||
this.isAlertVisible = true;
|
|
||||||
|
/**
|
||||||
|
* Handles errors and updates the component's state accordingly.
|
||||||
|
* @param {Error} err - The error object.
|
||||||
|
*/
|
||||||
|
handleError(err: unknown) {
|
||||||
|
if (
|
||||||
|
err instanceof Error &&
|
||||||
|
err.message ===
|
||||||
|
"Attempted to load account records with no identity available."
|
||||||
|
) {
|
||||||
|
this.limitsMessage = "No identity.";
|
||||||
|
this.loadingLimits = false;
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Creating Account",
|
||||||
|
text: "Clear your cache and start over (after data backup).",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error("Telling user to clear cache at page create because:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,82 +571,227 @@ export default class AccountViewView extends Vue {
|
|||||||
showContactGivesInline: this.showContactGives,
|
showContactGivesInline: this.showContactGives,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertMessage =
|
this.$notify(
|
||||||
"Clear your cache and start over (after data backup). See console log for more info.";
|
{
|
||||||
console.error("Telling user to clear cache because:", err);
|
group: "alert",
|
||||||
this.alertTitle = "Error Creating Account";
|
type: "danger",
|
||||||
this.isAlertVisible = true;
|
title: "Error Updating Contact Setting",
|
||||||
|
text: "Clear your cache and start over (after data backup).",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to clear cache after contact setting update because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously exports the database into a downloadable JSON file.
|
||||||
|
*
|
||||||
|
* @throws Will notify the user if there is an export error.
|
||||||
|
*/
|
||||||
public async exportDatabase() {
|
public async exportDatabase() {
|
||||||
try {
|
try {
|
||||||
const blob = await db.export({ prettyJson: true });
|
// Generate the blob from the database
|
||||||
const url = URL.createObjectURL(blob);
|
const blob = await this.generateDatabaseBlob();
|
||||||
|
|
||||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
// Create a temporary URL for the blob
|
||||||
downloadAnchor.href = url;
|
const url = this.createBlobURL(blob);
|
||||||
downloadAnchor.download = db.name + "-backup.json";
|
|
||||||
downloadAnchor.click();
|
|
||||||
|
|
||||||
|
// Trigger the download
|
||||||
|
this.downloadDatabaseBackup(url);
|
||||||
|
|
||||||
|
// Revoke the temporary URL
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
this.alertTitle = "Download Started";
|
// Notify the user that the download has started
|
||||||
this.alertMessage = "See your downloads directory for the backup.";
|
this.notifyDownloadStarted();
|
||||||
this.isAlertVisible = true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.alertTitle = "Export Error";
|
this.handleExportError(error);
|
||||||
this.alertMessage = "See console logs for more info.";
|
|
||||||
this.isAlertVisible = true;
|
|
||||||
console.error("Export Error:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a blob object representing the database.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Blob>} The generated blob object.
|
||||||
|
*/
|
||||||
|
private async generateDatabaseBlob(): Promise<Blob> {
|
||||||
|
return await db.export({ prettyJson: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a temporary URL for a blob object.
|
||||||
|
*
|
||||||
|
* @param {Blob} blob - The blob object.
|
||||||
|
* @returns {string} The temporary URL for the blob.
|
||||||
|
*/
|
||||||
|
private createBlobURL(blob: Blob): string {
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the download of the database backup.
|
||||||
|
*
|
||||||
|
* @param {string} url - The temporary URL for the blob.
|
||||||
|
*/
|
||||||
|
private downloadDatabaseBackup(url: string) {
|
||||||
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||||
|
downloadAnchor.href = url;
|
||||||
|
downloadAnchor.download = `${db.name}-backup.json`;
|
||||||
|
downloadAnchor.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the user that the download has started.
|
||||||
|
*/
|
||||||
|
private notifyDownloadStarted() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Download Started",
|
||||||
|
text: "See your downloads directory for the backup.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles errors during the database export process.
|
||||||
|
*
|
||||||
|
* @param {Error} error - The error object.
|
||||||
|
*/
|
||||||
|
private handleExportError(error: unknown) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Export Error",
|
||||||
|
text: "See console logs for more info.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error("Export Error:", error);
|
||||||
|
}
|
||||||
|
|
||||||
async checkLimits() {
|
async checkLimits() {
|
||||||
const url = this.apiServer + "/api/report/rateLimits";
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
await accountsDB.open();
|
if (identity) {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
this.checkLimitsFor(identity);
|
||||||
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",
|
* Asynchronously checks rate limits for the given identity.
|
||||||
Authorization: "Bearer " + token,
|
*
|
||||||
};
|
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
|
||||||
|
*/
|
||||||
|
public async checkLimitsFor(identity: IIdentifier) {
|
||||||
|
this.loadingLimits = true;
|
||||||
|
this.limitsMessage = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.fetchRateLimits(identity);
|
||||||
// axios throws an exception on a 400
|
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.limits = resp.data;
|
this.limits = resp.data;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error) {
|
||||||
const serverError = error as AxiosError;
|
this.handleRateLimitsError(error);
|
||||||
|
}
|
||||||
|
|
||||||
this.alertTitle = "Error from Server";
|
this.loadingLimits = false;
|
||||||
console.error("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;
|
* Fetches rate limits from the server.
|
||||||
if (data.error.message) {
|
*
|
||||||
this.alertMessage = data.error.message;
|
* @param {IIdentifier} identity - The identity object to check rate limits for.
|
||||||
|
* @returns {Promise<AxiosResponse>} The Axios response object.
|
||||||
|
*/
|
||||||
|
private async fetchRateLimits(identity: IIdentifier) {
|
||||||
|
const url = `${this.apiServer}/api/report/rateLimits`;
|
||||||
|
const headers = await this.getHeaders(identity);
|
||||||
|
return await this.axios.get(url, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles errors that occur while fetching rate limits.
|
||||||
|
*
|
||||||
|
* @param {AxiosError | Error} error - The error object.
|
||||||
|
*/
|
||||||
|
private handleRateLimitsError(error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
const data = error.response?.data as ErrorResponse;
|
||||||
|
this.limitsMessage = data?.error?.message || "Bad server response.";
|
||||||
|
console.error("Bad response retrieving limits:", error);
|
||||||
|
} else if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message ===
|
||||||
|
"Attempted to load Give records with no identity available."
|
||||||
|
) {
|
||||||
|
this.limitsMessage = "No identity.";
|
||||||
} else {
|
} else {
|
||||||
this.alertMessage = "Bad server response. See logs for details.";
|
// Handle other unknown errors
|
||||||
}
|
|
||||||
this.isAlertVisible = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchAccount(accountNum: number) {
|
/**
|
||||||
|
* Asynchronously switches the active account based on the provided account number.
|
||||||
|
*
|
||||||
|
* @param {number} accountNum - The account number to switch to. 0 means none.
|
||||||
|
*/
|
||||||
|
public async switchAccount(accountNum: number) {
|
||||||
|
await db.open(); // Assumes db needs to be open for both cases
|
||||||
|
|
||||||
|
if (accountNum === 0) {
|
||||||
|
this.switchToNoAccount();
|
||||||
|
} else {
|
||||||
|
await this.switchToAccountNumber(accountNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to no active account and clears relevant properties.
|
||||||
|
*/
|
||||||
|
private async switchToNoAccount() {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: undefined });
|
||||||
|
this.clearActiveAccountProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears properties related to the active account.
|
||||||
|
*/
|
||||||
|
private clearActiveAccountProperties() {
|
||||||
|
this.activeDid = "";
|
||||||
|
this.derivationPath = "";
|
||||||
|
this.publicHex = "";
|
||||||
|
this.publicBase64 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to an account based on its number in the list.
|
||||||
|
*
|
||||||
|
* @param {number} accountNum - The account number to switch to.
|
||||||
|
*/
|
||||||
|
private async switchToAccountNumber(accountNum: number) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = accounts[accountNum - 1];
|
const account = accounts[accountNum - 1];
|
||||||
|
|
||||||
await db.open();
|
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
activeDid: account.did,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
this.updateActiveAccountProperties(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates properties related to the active account.
|
||||||
|
*
|
||||||
|
* @param {AccountType} account - The account object.
|
||||||
|
*/
|
||||||
|
private updateActiveAccountProperties(account: IAccount) {
|
||||||
this.activeDid = account.did;
|
this.activeDid = account.did;
|
||||||
this.derivationPath = account.derivationPath;
|
this.derivationPath = account.derivationPath;
|
||||||
this.publicHex = account.publicKeyHex;
|
this.publicHex = account.publicKeyHex;
|
||||||
@@ -547,34 +813,8 @@ export default class AccountViewView extends Vue {
|
|||||||
this.apiServer = this.apiServerInput;
|
this.apiServer = this.apiServerInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
setApiServerInput(value) {
|
setApiServerInput(value: string) {
|
||||||
this.apiServerInput = value;
|
this.apiServerInput = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
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>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form>
|
|
||||||
<p class="text-center text-xl mb-4 font-light">
|
<p class="text-center text-xl mb-4 font-light">
|
||||||
Would you like to add <i>Firstname</i> to your network?
|
Would you like to add <i>Firstname</i> to your network?
|
||||||
</p>
|
</p>
|
||||||
@@ -43,14 +42,13 @@
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ConfirmContactView extends Vue {}
|
export default class ConfirmContactView extends Vue {}
|
||||||
|
|||||||
@@ -1,134 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<ul class="flex text-2xl p-2 gap-2">
|
<!-- Breadcrumb -->
|
||||||
<!-- Home Feed -->
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
|
<!-- Back -->
|
||||||
><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
|
<router-link
|
||||||
:to="{ name: 'contacts' }"
|
:to="{ name: 'contacts' }"
|
||||||
class="block text-center py-3 px-1"
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
><fa icon="users" class="fa-fw"></fa
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
></router-link>
|
></router-link>
|
||||||
</li>
|
</h1>
|
||||||
<!-- Profile -->
|
</div>
|
||||||
<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">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Given with {{ contact?.name }}
|
Given with {{ contact?.name }}
|
||||||
</h1>
|
</h1>
|
||||||
|
<div class="flex justify-around">
|
||||||
|
<span />
|
||||||
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<div>
|
<table
|
||||||
<div class="border-b border-slate-300 flex">
|
class="table-auto w-full border-t border-slate-300 text-sm sm:text-base text-center"
|
||||||
<div class="w-1/4"></div>
|
>
|
||||||
<div class="w-1/4">from them</div>
|
<thead class="bg-slate-100">
|
||||||
<div class="w-1/4"></div>
|
<tr class="border-b border-slate-300">
|
||||||
<div class="w-1/4">to them</div>
|
<th></th>
|
||||||
</div>
|
<th class="px-1 py-2">From Them</th>
|
||||||
<div
|
<th></th>
|
||||||
class="border-b border-slate-300 flex"
|
<th class="px-1 py-2">To Them</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
v-for="record in giveRecords"
|
v-for="record in giveRecords"
|
||||||
:key="record.id"
|
:key="record.id"
|
||||||
|
class="border-b border-slate-300"
|
||||||
>
|
>
|
||||||
<div class="w-1/4">
|
<td class="p-1 text-xs sm:text-sm text-left text-slate-500">
|
||||||
{{ new Date(record.issuedAt).toLocaleString() }}
|
{{ new Date(record.issuedAt).toLocaleString() }}
|
||||||
</div>
|
</td>
|
||||||
<div class="w-1/4">
|
<td class="p-1">
|
||||||
<span v-if="record.agentDid == contact.did">
|
<span v-if="record.agentDid == contact.did">
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
{{ record.amount }} {{ record.unit }}
|
{{ record.amount }} {{ record.unit }}
|
||||||
<span v-if="record.amountConfirmed" class="tooltip">
|
<span v-if="record.amountConfirmed" title="Confirmed">
|
||||||
<fa icon="circle-check" class="text-green-600 fa-fw ml-1" />
|
<fa icon="circle-check" class="text-green-600 fa-fw" />
|
||||||
<span class="tooltiptext">Confirmed</span>
|
|
||||||
</span>
|
</span>
|
||||||
<button v-else class="tooltip" @click="confirm(record)">
|
<button v-else @click="confirm(record)" title="Unconfirmed">
|
||||||
<fa icon="circle" class="text-blue-600 fa-fw ml-1" />
|
<fa icon="circle" class="text-blue-600 fa-fw" />
|
||||||
<span class="tooltiptext">Unconfirmed</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<div class="italic text-xs sm:text-sm text-slate-500">
|
||||||
{{ record.description }}
|
{{ record.description }}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/8">
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="p-1">
|
||||||
<span v-if="record.agentDid == contact.did">
|
<span v-if="record.agentDid == contact.did">
|
||||||
<fa icon="long-arrow-alt-left" class="text-slate-900 fa-fw ml-1" />
|
<fa icon="arrow-left" class="text-slate-400 fa-fw" />
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
|
<fa icon="arrow-right" class="text-slate-400 fa-fw" />
|
||||||
<fa icon="long-arrow-alt-right" class="text-slate-900 fa-fw ml-1" />
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</td>
|
||||||
<div class="w-1/4">
|
<td class="p-1">
|
||||||
<span v-if="record.agentDid != contact.did">
|
<span v-if="record.agentDid != contact.did">
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
{{ record.amount }} {{ record.unit }}
|
{{ record.amount }} {{ record.unit }}
|
||||||
<span v-if="record.amountConfirmed" class="tooltip">
|
<span v-if="record.amountConfirmed" title="Confirmed">
|
||||||
<fa icon="circle-check" class="text-green-600 fa-fw ml-1" />
|
<fa icon="circle-check" class="text-green-600 fa-fw" />
|
||||||
<span class="tooltiptext">Confirmed</span>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<!-- This same popup code is in many files. -->
|
|
||||||
<div v-bind:class="computedAlertClassNames()">
|
|
||||||
<button
|
<button
|
||||||
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
v-else
|
||||||
@click="onClickClose()"
|
@click="cannotConfirmMessage()"
|
||||||
|
title="Unconfirmed"
|
||||||
>
|
>
|
||||||
<fa icon="xmark"></fa>
|
<fa icon="circle" class="text-slate-600 fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
|
||||||
<p>{{ alertMessage }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="italic text-xs sm:text-sm text-slate-500">
|
||||||
|
{{ record.description }}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Options, Vue } from "vue-class-component";
|
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||||
@@ -140,16 +109,58 @@ import {
|
|||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
|
||||||
@Options({})
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({ components: { QuickNav } })
|
||||||
export default class ContactsView extends Vue {
|
export default class ContactsView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
contact: Contact | null = null;
|
contact: Contact | null = null;
|
||||||
giveRecords: Array<GiveServerRecord> = [];
|
giveRecords: Array<GiveServerRecord> = [];
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity: IIdentifier) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
|
||||||
async created() {
|
async created() {
|
||||||
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const contactDid = this.$route.query.contactDid as string;
|
const contactDid = this.$route.query.contactDid as string;
|
||||||
this.contact = (await db.contacts.get(contactDid)) || null;
|
this.contact = (await db.contacts.get(contactDid)) || null;
|
||||||
@@ -161,30 +172,33 @@ export default class ContactsView extends Vue {
|
|||||||
if (this.activeDid && this.contact) {
|
if (this.activeDid && this.contact) {
|
||||||
this.loadGives(this.activeDid, this.contact);
|
this.loadGives(this.activeDid, this.contact);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
err.userMessage ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadGives(activeDid: string, contact: 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");
|
|
||||||
|
|
||||||
// load all the time I have given to them
|
|
||||||
try {
|
try {
|
||||||
let result = [];
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
let result: Array<GiveServerRecord> = [];
|
||||||
const url =
|
const url =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/gives?agentDid=" +
|
"/api/v2/report/gives?agentDid=" +
|
||||||
encodeURIComponent(identity.did) +
|
encodeURIComponent(identity.did) +
|
||||||
"&recipientDid=" +
|
"&recipientDid=" +
|
||||||
encodeURIComponent(contact.did);
|
encodeURIComponent(contact.did);
|
||||||
const token = await accessToken(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
result = resp.data.data;
|
result = resp.data.data;
|
||||||
@@ -192,12 +206,17 @@ export default class ContactsView extends Vue {
|
|||||||
console.error(
|
console.error(
|
||||||
"Got bad response status & data of",
|
"Got bad response status & data of",
|
||||||
resp.status,
|
resp.status,
|
||||||
resp.data
|
resp.data,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Got an error retrieving your given time from the server.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
);
|
);
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
this.alertMessage =
|
|
||||||
"Got an error retrieving your given time from the server.";
|
|
||||||
this.isAlertVisible = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const url2 =
|
const url2 =
|
||||||
@@ -206,11 +225,7 @@ export default class ContactsView extends Vue {
|
|||||||
encodeURIComponent(contact.did) +
|
encodeURIComponent(contact.did) +
|
||||||
"&recipientDid=" +
|
"&recipientDid=" +
|
||||||
encodeURIComponent(identity.did);
|
encodeURIComponent(identity.did);
|
||||||
const token2 = await accessToken(identity);
|
const headers2 = await this.getHeaders(identity);
|
||||||
const headers2 = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token2,
|
|
||||||
};
|
|
||||||
const resp2 = await this.axios.get(url2, { headers: headers2 });
|
const resp2 = await this.axios.get(url2, { headers: headers2 });
|
||||||
if (resp2.status === 200) {
|
if (resp2.status === 200) {
|
||||||
result = R.concat(result, resp2.data.data);
|
result = R.concat(result, resp2.data.data);
|
||||||
@@ -218,24 +233,35 @@ export default class ContactsView extends Vue {
|
|||||||
console.error(
|
console.error(
|
||||||
"Got bad response status & data of",
|
"Got bad response status & data of",
|
||||||
resp2.status,
|
resp2.status,
|
||||||
resp2.data
|
resp2.data,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Got an error retrieving your given time from the server.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
);
|
);
|
||||||
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(
|
const sortedResult: Array<GiveServerRecord> = R.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
|
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
|
||||||
result
|
result,
|
||||||
);
|
);
|
||||||
this.giveRecords = sortedResult;
|
this.giveRecords = sortedResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = error as string;
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: error as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,10 +290,7 @@ export default class ContactsView extends Vue {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create a signature using private key of identity
|
// Create a signature using private key of identity
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
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) {
|
if (identity.keys[0].privateKeyHex !== null) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
||||||
@@ -291,7 +314,6 @@ export default class ContactsView extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
//console.log("Got resp data:", resp.data);
|
|
||||||
if (resp.data?.success) {
|
if (resp.data?.success) {
|
||||||
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
|
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
|
||||||
}
|
}
|
||||||
@@ -308,43 +330,29 @@ export default class ContactsView extends Vue {
|
|||||||
userMessage = error as string;
|
userMessage = error as string;
|
||||||
}
|
}
|
||||||
// Now set that error for the user to see.
|
// Now set that error for the user to see.
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = userMessage;
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cannotConfirmMessage() {
|
cannotConfirmMessage() {
|
||||||
this.alertTitle = "Not Allowed";
|
this.$notify(
|
||||||
this.alertMessage = "Only the recipient can confirm final receipt.";
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
}
|
type: "danger",
|
||||||
|
title: "Not Allowed",
|
||||||
// This same popup code is in many files.
|
text: "Only the recipient can confirm final receipt.",
|
||||||
alertTitle = "";
|
},
|
||||||
alertMessage = "";
|
-1,
|
||||||
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>
|
||||||
|
|||||||
295
src/views/ContactGiftingView.vue
Normal file
295
src/views/ContactGiftingView.vue
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Home"></QuickNav>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
<!-- Back -->
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'home' }"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
|
></router-link>
|
||||||
|
|
||||||
|
Give to Contacts
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Search -->
|
||||||
|
|
||||||
|
<!-- Initial Loading Animation -->
|
||||||
|
|
||||||
|
<!-- Results List -->
|
||||||
|
<ul class="border-t border-slate-300">
|
||||||
|
<li class="border-b border-slate-300 py-3">
|
||||||
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
|
<span class="grow italic text-slate-500"
|
||||||
|
><EntityIcon
|
||||||
|
:entityId="null"
|
||||||
|
:iconSize="32"
|
||||||
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||||
|
></EntityIcon>
|
||||||
|
Anonymous
|
||||||
|
</span>
|
||||||
|
<span class="text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openDialog()"
|
||||||
|
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="gift" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="contact in allContacts"
|
||||||
|
:key="contact.did"
|
||||||
|
class="border-b border-slate-300 py-3"
|
||||||
|
>
|
||||||
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
|
<span class="grow font-semibold"
|
||||||
|
><EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="32"
|
||||||
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||||
|
></EntityIcon>
|
||||||
|
{{ contact.name || "(no name)" }}
|
||||||
|
</span>
|
||||||
|
<span class="text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openDialog(contact)"
|
||||||
|
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="gift" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<GiftedDialog
|
||||||
|
ref="customDialog"
|
||||||
|
@dialog-result="handleDialogResult"
|
||||||
|
message="Received from"
|
||||||
|
>
|
||||||
|
</GiftedDialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import { db, accountsDB } from "@/db/index";
|
||||||
|
import { AccountsSchema } from "@/db/tables/accounts";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
|
CreateAndSubmitGiveResult,
|
||||||
|
ErrorResult,
|
||||||
|
GiverInputInfo,
|
||||||
|
GiverOutputInfo,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||||
|
})
|
||||||
|
export default class ContactGiftingView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
activeDid = "";
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
apiServer = "";
|
||||||
|
accounts: typeof AccountsSchema;
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity: IIdentifier) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
err.message ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openDialog(giver: GiverInputInfo) {
|
||||||
|
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDialogResult(result: GiverOutputInfo) {
|
||||||
|
if (result.action === "confirm") {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.recordGive(
|
||||||
|
result.giver?.did,
|
||||||
|
result.description,
|
||||||
|
result.hours,
|
||||||
|
).then(() => {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// action was "cancel" so do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param giverDid may be null
|
||||||
|
* @param description may be an empty string
|
||||||
|
* @param hours may be 0
|
||||||
|
*/
|
||||||
|
public async recordGive(
|
||||||
|
giverDid?: string,
|
||||||
|
description?: string,
|
||||||
|
hours?: number,
|
||||||
|
) {
|
||||||
|
if (!this.activeDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identity before you can record a give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description && !hours) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must enter a description or some number of hours.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
const result = await createAndSubmitGive(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
identity,
|
||||||
|
giverDid,
|
||||||
|
this.activeDid,
|
||||||
|
description,
|
||||||
|
hours,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.isGiveCreationError(result)) {
|
||||||
|
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||||
|
console.log("Error with give result:", result);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage || "There was an error recording the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: "That gift was recorded.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("Error with give caught:", error);
|
||||||
|
|
||||||
|
const message =
|
||||||
|
error.userMessage ||
|
||||||
|
error.response?.data?.error?.message ||
|
||||||
|
"There was an error recording the Give.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for readability
|
||||||
|
|
||||||
|
isGiveCreationError(result: CreateAndSubmitGiveResult) {
|
||||||
|
return result.type == "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) {
|
||||||
|
return (result as ErrorResult).error?.userMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,57 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
|
||||||
<ul class="flex text-2xl p-2 gap-2">
|
|
||||||
<!-- Home Feed -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
|
|
||||||
<fa icon="house-chimney" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Search -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'discover' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Projects -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'projects' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="folder-open" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Contacts -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contacts' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="users" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Profile -->
|
|
||||||
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="circle-user" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Contact Info
|
Your Contact Info
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -64,43 +17,60 @@
|
|||||||
:dotsOptions="{ type: 'square' }"
|
:dotsOptions="{ type: 'square' }"
|
||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- This same popup code is in many files. -->
|
|
||||||
<div v-bind:class="computedAlertClassNames()">
|
|
||||||
<button
|
|
||||||
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
|
||||||
@click="onClickClose()"
|
|
||||||
>
|
|
||||||
<fa icon="xmark"></fa>
|
|
||||||
</button>
|
|
||||||
<p>{{ alertMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { SimpleSigner } from "@/libs/crypto";
|
import { SimpleSigner } from "@/libs/crypto";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QRCodeVue3,
|
QRCodeVue3,
|
||||||
|
QuickNav,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ContactQRScanShow extends Vue {
|
export default class ContactQRScanShow extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
const account: Account | undefined = R.find(
|
||||||
|
(acc) => acc.did === activeDid,
|
||||||
|
accounts,
|
||||||
|
);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
@@ -111,10 +81,17 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
this.alertMessage = "You have no identity yet.";
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "",
|
||||||
|
text: "You have no identity yet.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const identity = JSON.parse(account.identity);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
|
||||||
const publicKeyHex = identity.keys[0].publicKeyHex;
|
const publicKeyHex = identity.keys[0].publicKeyHex;
|
||||||
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
||||||
const contactInfo = {
|
const contactInfo = {
|
||||||
@@ -139,27 +116,5 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.qrValue = viewPrefix + vcJwt;
|
this.qrValue = viewPrefix + vcJwt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
alertMessage = "";
|
|
||||||
public onClickClose() {
|
|
||||||
this.alertMessage = "";
|
|
||||||
}
|
|
||||||
public computedAlertClassNames() {
|
|
||||||
return {
|
|
||||||
hidden: !this.alertMessage,
|
|
||||||
"dismissable-alert": true,
|
|
||||||
"bg-slate-100": true,
|
|
||||||
"p-5": true,
|
|
||||||
rounded: true,
|
|
||||||
"drop-shadow-lg": true,
|
|
||||||
fixed: true,
|
|
||||||
"top-3": true,
|
|
||||||
"inset-x-3": true,
|
|
||||||
"transition-transform": true,
|
|
||||||
"ease-in": true,
|
|
||||||
"duration-300": true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form>
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-2">Scan a QR Code…</h3>
|
<h3 class="text-sm uppercase font-semibold mb-2">Scan a QR Code…</h3>
|
||||||
<div class="bg-black rounded overflow-hidden relative mb-4">
|
<div class="bg-black rounded overflow-hidden relative mb-4">
|
||||||
<img src="https://picsum.photos/400/400?random=1" class="w-full" />
|
<img src="https://picsum.photos/400/400?random=1" class="w-full" />
|
||||||
@@ -25,9 +24,7 @@
|
|||||||
<!-- Reft -->
|
<!-- Reft -->
|
||||||
<div class="absolute top-1/4 bottom-1/4 left-0 bg-black/50 w-1/4"></div>
|
<div class="absolute top-1/4 bottom-1/4 left-0 bg-black/50 w-1/4"></div>
|
||||||
<!-- Right -->
|
<!-- Right -->
|
||||||
<div
|
<div class="absolute top-1/4 bottom-1/4 right-0 bg-black/50 w-1/4"></div>
|
||||||
class="absolute top-1/4 bottom-1/4 right-0 bg-black/50 w-1/4"
|
|
||||||
></div>
|
|
||||||
<!-- Bottom -->
|
<!-- Bottom -->
|
||||||
<div class="absolute bottom-0 left-0 right-0 bg-black/50 h-1/4"></div>
|
<div class="absolute bottom-0 left-0 right-0 bg-black/50 h-1/4"></div>
|
||||||
|
|
||||||
@@ -50,9 +47,7 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-2">
|
<h3 class="text-sm uppercase font-semibold mb-2">…or Enter Contact Data</h3>
|
||||||
…or Enter Contact Data
|
|
||||||
</h3>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name (optional)"
|
placeholder="Name (optional)"
|
||||||
@@ -82,14 +77,13 @@
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ContactScanView extends Vue {}
|
export default class ContactScanView extends Vue {}
|
||||||
|
|||||||
@@ -1,48 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
<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">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
@@ -109,18 +66,27 @@
|
|||||||
: "Unconfirmed"
|
: "Unconfirmed"
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
|
<br />
|
||||||
|
(Only hours shown)
|
||||||
|
<br />
|
||||||
|
(Only recent shown)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul class="">
|
<ul v-if="contacts.length > 0" class="border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300"
|
class="border-b border-slate-300 pt-2.5 pb-4"
|
||||||
v-for="contact in contacts"
|
v-for="contact in contacts"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
>
|
>
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<h2 class="text-base font-semibold">
|
<h2 class="text-base font-semibold">
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="24"
|
||||||
|
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||||
|
></EntityIcon>
|
||||||
{{ contact.name || "(no name)" }}
|
{{ contact.name || "(no name)" }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-sm truncate">{{ contact.did }}</div>
|
<div class="text-sm truncate">{{ contact.did }}</div>
|
||||||
@@ -128,45 +94,68 @@
|
|||||||
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
||||||
<button
|
<button
|
||||||
v-if="contact.seesMe"
|
v-if="contact.seesMe"
|
||||||
class="tooltip"
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
@click="setVisibility(contact, false)"
|
@click="setVisibility(contact, false)"
|
||||||
|
title="They can see you"
|
||||||
>
|
>
|
||||||
<fa icon="eye" class="text-slate-900 fa-fw ml-1" />
|
<fa icon="eye" class="fa-fw" />
|
||||||
<span class="tooltiptext">They can see you</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button v-else class="tooltip" @click="setVisibility(contact, true)">
|
<button
|
||||||
<span class="tooltiptext">They cannot see you</span>
|
v-else
|
||||||
<fa icon="eye-slash" class="text-slate-900 fa-fw ml-1" />
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
|
@click="setVisibility(contact, true)"
|
||||||
|
title="They cannot see you"
|
||||||
|
>
|
||||||
|
<fa icon="eye-slash" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="tooltip" @click="checkVisibility(contact)">
|
<button
|
||||||
<span class="tooltiptext">Check Visibility</span>
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
<fa icon="rotate" class="text-slate-900 fa-fw ml-1" />
|
@click="checkVisibility(contact)"
|
||||||
|
title="Check Visibility"
|
||||||
|
>
|
||||||
|
<fa icon="rotate" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="contact.registered" class="tooltip">
|
<button
|
||||||
<span class="tooltiptext">Registered</span>
|
@click="register(contact)"
|
||||||
<fa icon="person-circle-check" class="text-slate-900 fa-fw ml-1" />
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
</button>
|
>
|
||||||
<button v-else @click="register(contact)" class="tooltip">
|
|
||||||
<span class="tooltiptext">Registration Unknown</span>
|
|
||||||
<fa
|
<fa
|
||||||
|
v-if="contact.registered"
|
||||||
|
icon="person-circle-check"
|
||||||
|
class="fa-fw"
|
||||||
|
title="Registered"
|
||||||
|
/>
|
||||||
|
<fa
|
||||||
|
v-else
|
||||||
icon="person-circle-question"
|
icon="person-circle-question"
|
||||||
class="text-slate-900 fa-fw ml-1"
|
class="fa-fw"
|
||||||
|
title="Registration Unknown"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="deleteContact(contact)" class="px-9 tooltip">
|
<button
|
||||||
<span class="tooltiptext">Delete!</span>
|
@click="deleteContact(contact)"
|
||||||
<fa icon="trash-can" class="text-red-600 fa-fw ml-1" />
|
class="text-sm uppercase bg-red-600 text-white px-2 py-1.5 rounded-md"
|
||||||
|
title="Delete"
|
||||||
|
>
|
||||||
|
<fa icon="trash-can" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="showGiveNumbers" class="float-right">
|
<div
|
||||||
<div class="float-right">
|
v-if="showGiveNumbers && contact.did != activeDid"
|
||||||
<div class="tooltip">
|
class="ml-auto flex gap-1.5"
|
||||||
to:
|
>
|
||||||
|
<button
|
||||||
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
|
||||||
|
@click="onClickAddGive(activeDid, contact.did)"
|
||||||
|
title="givenByMeDescriptions[contact.did]"
|
||||||
|
>
|
||||||
|
To:
|
||||||
{{
|
{{
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
this.showGiveTotals
|
this.showGiveTotals
|
||||||
@@ -177,21 +166,15 @@
|
|||||||
: (givenByMeUnconfirmed[contact.did] || 0)
|
: (givenByMeUnconfirmed[contact.did] || 0)
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
}}
|
}}
|
||||||
<span
|
<fa icon="plus" />
|
||||||
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>
|
</button>
|
||||||
</div>
|
|
||||||
<div class="tooltip px-2">
|
<button
|
||||||
from:
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
|
||||||
|
@click="onClickAddGive(contact.did, activeDid)"
|
||||||
|
title="givenToMeDescriptions[contact.did]"
|
||||||
|
>
|
||||||
|
From:
|
||||||
{{
|
{{
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
this.showGiveTotals
|
this.showGiveTotals
|
||||||
@@ -202,47 +185,26 @@
|
|||||||
: (givenToMeUnconfirmed[contact.did] || 0)
|
: (givenToMeUnconfirmed[contact.did] || 0)
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
}}
|
}}
|
||||||
<span
|
<fa icon="plus" />
|
||||||
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>
|
</button>
|
||||||
</div>
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'contact-amounts',
|
name: 'contact-amounts',
|
||||||
query: { contactDid: contact.did },
|
query: { contactDid: contact.did },
|
||||||
}"
|
}"
|
||||||
class="tooltip"
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
|
title="See all given activity"
|
||||||
>
|
>
|
||||||
<fa icon="file-lines" class="text-slate-600 fa-fw ml-1" />
|
<fa icon="file-lines" class="fa-fw" />
|
||||||
<span class="tooltiptext-left">See All Given Activity</span>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p v-else>This identity has no contacts.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- This same popup code is in many files. -->
|
|
||||||
<div v-bind:class="computedAlertClassNames()">
|
|
||||||
<button
|
|
||||||
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
|
||||||
@click="onClickClose()"
|
|
||||||
>
|
|
||||||
<fa icon="xmark"></fa>
|
|
||||||
</button>
|
|
||||||
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
|
||||||
<p>{{ alertMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -250,9 +212,7 @@ import { AxiosError } from "axios";
|
|||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||||
@@ -262,14 +222,26 @@ import {
|
|||||||
RegisterVerifiableCredential,
|
RegisterVerifiableCredential,
|
||||||
SERVICE_ID,
|
SERVICE_ID,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
|
||||||
@Options({
|
interface Notification {
|
||||||
components: {},
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ContactsView extends Vue {
|
export default class ContactsView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
contacts: Array<Contact> = [];
|
contacts: Array<Contact> = [];
|
||||||
@@ -292,7 +264,6 @@ export default class ContactsView extends Vue {
|
|||||||
showGiveTotals = true;
|
showGiveTotals = true;
|
||||||
showGiveConfirmed = true;
|
showGiveConfirmed = true;
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
@@ -306,130 +277,139 @@ export default class ContactsView extends Vue {
|
|||||||
const allContacts = await db.contacts.toArray();
|
const allContacts = await db.contacts.toArray();
|
||||||
this.contacts = R.sort(
|
this.contacts = R.sort(
|
||||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||||
allContacts
|
allContacts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity: IIdentifier) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeadersAndIdentity(activeDid: string) {
|
||||||
|
const identity = await this.getIdentity(activeDid);
|
||||||
|
const headers = await this.getHeaders(identity);
|
||||||
|
|
||||||
|
return { headers, identity };
|
||||||
|
}
|
||||||
|
|
||||||
async loadGives() {
|
async loadGives() {
|
||||||
await accountsDB.open();
|
const handleResponse = (
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
resp: { status: number; data: { data: GiveServerRecord[] } },
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
descriptions: Record<string, string>,
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
confirmed: Record<string, number>,
|
||||||
|
unconfirmed: Record<string, number>,
|
||||||
if (!identity) {
|
useRecipient: boolean,
|
||||||
console.error(
|
) => {
|
||||||
"Attempted to load Give records with no identity available."
|
if (resp.status === 200) {
|
||||||
);
|
const allData = resp.data.data;
|
||||||
return;
|
for (const give of allData) {
|
||||||
|
const otherDid = useRecipient ? give.recipientDid : give.agentDid;
|
||||||
|
if (give.unit === "HUR") {
|
||||||
|
if (give.amountConfirmed) {
|
||||||
|
const prevAmount = confirmed[otherDid] || 0;
|
||||||
|
confirmed[otherDid] = prevAmount + give.amount;
|
||||||
|
} else {
|
||||||
|
const prevAmount = unconfirmed[otherDid] || 0;
|
||||||
|
unconfirmed[otherDid] = prevAmount + give.amount;
|
||||||
}
|
}
|
||||||
|
if (!descriptions[otherDid] && give.description) {
|
||||||
|
descriptions[otherDid] = give.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Got bad response status & data of",
|
||||||
|
resp.status,
|
||||||
|
resp.data,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text:
|
||||||
|
"Got an error retrieving your " +
|
||||||
|
(useRecipient ? "given" : "received") +
|
||||||
|
" time from the server.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// load all the time I have given
|
|
||||||
try {
|
try {
|
||||||
const url =
|
const { headers } = await this.getHeadersAndIdentity(this.activeDid);
|
||||||
|
const givenByUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/gives?agentDid=" +
|
"/api/v2/report/gives?agentDid=" +
|
||||||
encodeURIComponent(identity.did);
|
encodeURIComponent(this.activeDid);
|
||||||
const token = await accessToken(identity);
|
const givenToUrl =
|
||||||
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.error(
|
|
||||||
"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 =
|
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/gives?recipientDid=" +
|
"/api/v2/report/gives?recipientDid=" +
|
||||||
encodeURIComponent(identity.did);
|
encodeURIComponent(this.activeDid);
|
||||||
const token = await accessToken(identity);
|
|
||||||
const headers = {
|
const [givenByMeResp, givenToMeResp] = await Promise.all([
|
||||||
"Content-Type": "application/json",
|
this.axios.get(givenByUrl, { headers }),
|
||||||
Authorization: "Bearer " + token,
|
this.axios.get(givenToUrl, { headers }),
|
||||||
};
|
]);
|
||||||
const resp = await this.axios.get(url, { headers });
|
|
||||||
//console.log("All gifts you've recieved:", resp.data);
|
const givenByMeDescriptions = {};
|
||||||
if (resp.status === 200) {
|
const givenByMeConfirmed = {};
|
||||||
const contactDescriptions: Record<string, string> = {};
|
const givenByMeUnconfirmed = {};
|
||||||
const contactConfirmed: Record<string, number> = {};
|
handleResponse(
|
||||||
const contactUnconfirmed: Record<string, number> = {};
|
givenByMeResp,
|
||||||
const allData: Array<GiveServerRecord> = resp.data.data;
|
givenByMeDescriptions,
|
||||||
for (const give of allData) {
|
givenByMeConfirmed,
|
||||||
if (give.unit == "HUR") {
|
givenByMeUnconfirmed,
|
||||||
if (give.amountConfirmed) {
|
true,
|
||||||
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.error(
|
|
||||||
"Got bad response status & data of",
|
|
||||||
resp.status,
|
|
||||||
resp.data
|
|
||||||
);
|
);
|
||||||
this.alertTitle = "Error With Server";
|
this.givenByMeDescriptions = givenByMeDescriptions;
|
||||||
this.alertMessage =
|
this.givenByMeConfirmed = givenByMeConfirmed;
|
||||||
"Got an error retrieving your received time from the server.";
|
this.givenByMeUnconfirmed = givenByMeUnconfirmed;
|
||||||
this.isAlertVisible = true;
|
|
||||||
}
|
const givenToMeDescriptions = {};
|
||||||
|
const givenToMeConfirmed = {};
|
||||||
|
const givenToMeUnconfirmed = {};
|
||||||
|
handleResponse(
|
||||||
|
givenToMeResp,
|
||||||
|
givenToMeDescriptions,
|
||||||
|
givenToMeConfirmed,
|
||||||
|
givenToMeUnconfirmed,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
this.givenToMeDescriptions = givenToMeDescriptions;
|
||||||
|
this.givenToMeConfirmed = givenToMeConfirmed;
|
||||||
|
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = error as string;
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: error as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,7 +436,7 @@ export default class ContactsView extends Vue {
|
|||||||
const allContacts = this.contacts.concat([newContact]);
|
const allContacts = this.contacts.concat([newContact]);
|
||||||
this.contacts = R.sort(
|
this.contacts = R.sort(
|
||||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||||
allContacts
|
allContacts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +447,7 @@ export default class ContactsView extends Vue {
|
|||||||
this.nameForDid(this.contacts, contact.did) +
|
this.nameForDid(this.contacts, contact.did) +
|
||||||
" with DID " +
|
" with DID " +
|
||||||
contact.did +
|
contact.did +
|
||||||
" ?"
|
" ?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -481,21 +461,20 @@ export default class ContactsView extends Vue {
|
|||||||
confirm(
|
confirm(
|
||||||
"Are you sure you want to use one of your registrations for " +
|
"Are you sure you want to use one of your registrations for " +
|
||||||
this.nameForDid(this.contacts, contact.did) +
|
this.nameForDid(this.contacts, contact.did) +
|
||||||
"?"
|
(contact.registered
|
||||||
|
? " -- especially since they are already marked as registered"
|
||||||
|
: "") +
|
||||||
|
"?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
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 = {
|
const vcClaim: RegisterVerifiableCredential = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "RegisterAction",
|
"@type": "RegisterAction",
|
||||||
agent: { identifier: identity.did },
|
agent: { identifier: identity.did },
|
||||||
object: SERVICE_ID,
|
object: SERVICE_ID,
|
||||||
recipient: { identifier: contact.did },
|
participant: { identifier: contact.did },
|
||||||
};
|
};
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
@@ -521,22 +500,37 @@ export default class ContactsView extends Vue {
|
|||||||
// Make the xhr request payload
|
// Make the xhr request payload
|
||||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||||
const url = this.apiServer + "/api/v2/claim";
|
const url = this.apiServer + "/api/v2/claim";
|
||||||
const token = await accessToken(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
//console.log("Got resp data:", resp.data);
|
if (resp.data?.success?.embeddedRecordError) {
|
||||||
if (resp.data?.success?.handleId) {
|
let message = "There was some problem with the registration.";
|
||||||
|
if (typeof resp.data.success.embeddedRecordError == "string") {
|
||||||
|
message += " " + resp.data.success.embeddedRecordError;
|
||||||
|
}
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Registration Still Unknown",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else if (resp.data?.success?.handleId) {
|
||||||
contact.registered = true;
|
contact.registered = true;
|
||||||
db.contacts.update(contact.did, { registered: true });
|
db.contacts.update(contact.did, { registered: true });
|
||||||
|
|
||||||
this.alertTitle = "Registration Success";
|
this.$notify(
|
||||||
this.alertMessage = contact.name + " has been registered.";
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Registration Success",
|
||||||
|
text: contact.name + " has been registered.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let userMessage = "There was an error. See logs for more info.";
|
let userMessage = "There was an error. See logs for more info.";
|
||||||
@@ -551,9 +545,15 @@ export default class ContactsView extends Vue {
|
|||||||
userMessage = error as string;
|
userMessage = error as string;
|
||||||
}
|
}
|
||||||
// Now set that error for the user to see.
|
// Now set that error for the user to see.
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = userMessage;
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,16 +564,8 @@ export default class ContactsView extends Vue {
|
|||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/report/" +
|
"/api/report/" +
|
||||||
(visibility ? "canSeeMe" : "cannotSeeMe");
|
(visibility ? "canSeeMe" : "cannotSeeMe");
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const headers = await this.getHeaders(identity);
|
||||||
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 });
|
const payload = JSON.stringify({ did: contact.did });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -582,19 +574,39 @@ export default class ContactsView extends Vue {
|
|||||||
contact.seesMe = visibility;
|
contact.seesMe = visibility;
|
||||||
db.contacts.update(contact.did, { seesMe: visibility });
|
db.contacts.update(contact.did, { seesMe: visibility });
|
||||||
} else {
|
} else {
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
console.error("Bad response setting visibility: ", resp.data);
|
console.error("Bad response setting visibility: ", resp.data);
|
||||||
if (resp.data.error?.message) {
|
if (resp.data.error?.message) {
|
||||||
this.alertMessage = resp.data.error?.message;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: resp.data.error?.message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.alertMessage = "Bad server response of " + resp.status;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Bad server response of " + resp.status,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.isAlertVisible = true;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = err as string;
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: err as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,16 +615,8 @@ export default class ContactsView extends Vue {
|
|||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/report/canDidExplicitlySeeMe?did=" +
|
"/api/report/canDidExplicitlySeeMe?did=" +
|
||||||
encodeURIComponent(contact.did);
|
encodeURIComponent(contact.did);
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const headers = await this.getHeaders(identity);
|
||||||
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 {
|
try {
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
@@ -621,26 +625,52 @@ export default class ContactsView extends Vue {
|
|||||||
contact.seesMe = visibility;
|
contact.seesMe = visibility;
|
||||||
db.contacts.update(contact.did, { seesMe: visibility });
|
db.contacts.update(contact.did, { seesMe: visibility });
|
||||||
|
|
||||||
this.alertTitle = "Refreshed";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Refreshed",
|
||||||
|
text:
|
||||||
this.nameForContact(contact, true) +
|
this.nameForContact(contact, true) +
|
||||||
" can " +
|
" can " +
|
||||||
(visibility ? "" : "not ") +
|
(visibility ? "" : "not ") +
|
||||||
"see your activity.";
|
"see your activity.",
|
||||||
this.isAlertVisible = true;
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
if (resp.data.error?.message) {
|
if (resp.data.error?.message) {
|
||||||
this.alertMessage = resp.data.error?.message;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: resp.data.error?.message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.alertMessage = "Bad server response of " + resp.status;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Bad server response of " + resp.status,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.isAlertVisible = true;
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = err as string;
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: err as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,40 +690,61 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
|
async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
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 they have unconfirmed amounts, ask to confirm those first
|
||||||
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
|
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
|
||||||
|
const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are";
|
||||||
|
const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours";
|
||||||
if (
|
if (
|
||||||
confirm(
|
confirm(
|
||||||
"There are " +
|
"There " +
|
||||||
|
isare +
|
||||||
|
" " +
|
||||||
this.givenToMeUnconfirmed[fromDid] +
|
this.givenToMeUnconfirmed[fromDid] +
|
||||||
" unconfirmed hours from them." +
|
" unconfirmed " +
|
||||||
" Would you like to confirm some of those hours?"
|
hours +
|
||||||
|
" from them." +
|
||||||
|
" Would you like to confirm some of those hours?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: "contact-amounts",
|
name: "contact-amounts",
|
||||||
query: { contactDid: fromDid },
|
query: { contactDid: fromDid },
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.isNumeric(this.hourInput)) {
|
if (!this.isNumeric(this.hourInput)) {
|
||||||
this.alertTitle = "Input Error";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
"This is not a valid number of hours: " + this.hourInput;
|
group: "alert",
|
||||||
this.isAlertVisible = true;
|
type: "danger",
|
||||||
|
title: "Input Error",
|
||||||
|
text: "This is not a valid number of hours: " + this.hourInput,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else if (!parseFloat(this.hourInput)) {
|
} else if (!parseFloat(this.hourInput)) {
|
||||||
this.alertTitle = "Input Error";
|
this.$notify(
|
||||||
this.alertMessage = "Giving 0 hours does nothing.";
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Input Error",
|
||||||
|
text: "Giving 0 hours does nothing.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else if (!identity) {
|
} else if (!identity) {
|
||||||
this.alertTitle = "Status Error";
|
this.$notify(
|
||||||
this.alertMessage = "No identity is available.";
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Status Error",
|
||||||
|
text: "No identity is available.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// ask to confirm amount
|
// ask to confirm amount
|
||||||
let toFrom;
|
let toFrom;
|
||||||
@@ -712,10 +763,12 @@ export default class ContactsView extends Vue {
|
|||||||
confirm(
|
confirm(
|
||||||
"Are you sure you want to record " +
|
"Are you sure you want to record " +
|
||||||
this.hourInput +
|
this.hourInput +
|
||||||
" hours " +
|
" hour" +
|
||||||
|
(this.hourInput == "1" ? "" : "s") +
|
||||||
|
" " +
|
||||||
toFrom +
|
toFrom +
|
||||||
description +
|
description +
|
||||||
"?"
|
"?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.createAndSubmitGive(
|
this.createAndSubmitGive(
|
||||||
@@ -723,7 +776,7 @@ export default class ContactsView extends Vue {
|
|||||||
fromDid,
|
fromDid,
|
||||||
toDid,
|
toDid,
|
||||||
parseFloat(this.hourInput),
|
parseFloat(this.hourInput),
|
||||||
this.hourDescriptionInput
|
this.hourDescriptionInput,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -734,7 +787,7 @@ export default class ContactsView extends Vue {
|
|||||||
fromDid: string,
|
fromDid: string,
|
||||||
toDid: string,
|
toDid: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
description: string
|
description: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim: GiveVerifiableCredential = {
|
const vcClaim: GiveVerifiableCredential = {
|
||||||
@@ -772,19 +825,21 @@ export default class ContactsView extends Vue {
|
|||||||
|
|
||||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||||
const url = this.apiServer + "/api/v2/claim";
|
const url = this.apiServer + "/api/v2/claim";
|
||||||
const token = await accessToken(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
//console.log("Got resp data:", resp.data);
|
|
||||||
if (resp.data?.success?.handleId) {
|
if (resp.data?.success?.handleId) {
|
||||||
this.alertTitle = "Done";
|
this.$notify(
|
||||||
this.alertMessage = "Successfully logged time to the server.";
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Done",
|
||||||
|
text: "Successfully logged time to the server.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
|
||||||
if (fromDid === identity.did) {
|
if (fromDid === identity.did) {
|
||||||
const newList = R.clone(this.givenByMeUnconfirmed);
|
const newList = R.clone(this.givenByMeUnconfirmed);
|
||||||
newList[toDid] = (newList[toDid] || 0) + amount;
|
newList[toDid] = (newList[toDid] || 0) + amount;
|
||||||
@@ -808,9 +863,15 @@ export default class ContactsView extends Vue {
|
|||||||
userMessage = error as string;
|
userMessage = error as string;
|
||||||
}
|
}
|
||||||
// Now set that error for the user to see.
|
// Now set that error for the user to see.
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = userMessage;
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -828,32 +889,6 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
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() {
|
public showGiveAmountsClassNames() {
|
||||||
return {
|
return {
|
||||||
"bg-slate-500": this.showGiveTotals,
|
"bg-slate-500": this.showGiveTotals,
|
||||||
|
|||||||
@@ -1,47 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Discover"></QuickNav>
|
||||||
<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 bg-slate-400 text-white">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'discover' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="magnifying-glass" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Projects -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'projects' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="folder-open" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Contacts -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contacts' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="users" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Profile -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="circle-user" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
@@ -51,18 +9,20 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Quick Search -->
|
<!-- Quick Search -->
|
||||||
<form id="QuickSearch" class="mb-4 flex">
|
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
v-model="searchTerms"
|
||||||
placeholder="Search…"
|
placeholder="Search…"
|
||||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@click="searchAll()"
|
||||||
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||||
>
|
>
|
||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
<!-- Result Tabs -->
|
<!-- Result Tabs -->
|
||||||
<div class="text-center text-slate-500 border-b border-slate-300">
|
<div class="text-center text-slate-500 border-b border-slate-300">
|
||||||
@@ -70,95 +30,590 @@
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="inline-block py-3 rounded-t-lg border-b-2 active text-blue-600 border-blue-600 font-semibold"
|
@click="
|
||||||
|
projects = [];
|
||||||
|
isLocalActive = true;
|
||||||
|
isRemoteActive = false;
|
||||||
|
searchLocal();
|
||||||
|
"
|
||||||
|
v-bind:class="computedLocalTabClassNames()"
|
||||||
>
|
>
|
||||||
Nearby
|
Nearby
|
||||||
<span
|
<span
|
||||||
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||||
>20+</span
|
>{{ localCount }}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="inline-block py-3 rounded-t-lg border-b-2 border-transparent hover:text-slate-600 hover:border-slate-300"
|
v-bind:class="computedRemoteTabClassNames()"
|
||||||
|
@click="
|
||||||
|
projects = [];
|
||||||
|
isRemoteActive = true;
|
||||||
|
isLocalActive = false;
|
||||||
|
searchAll();
|
||||||
|
"
|
||||||
>
|
>
|
||||||
Remote
|
Anywhere
|
||||||
<span
|
<span
|
||||||
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||||
>13</span
|
>{{ remoteCount }}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLocalActive">
|
||||||
|
<div v-if="!isChoosingSearchBox">
|
||||||
|
<button
|
||||||
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="isChoosingSearchBox = true"
|
||||||
|
>
|
||||||
|
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
||||||
|
Choose Location Below for Nearby Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="storeSearchBox"
|
||||||
|
>
|
||||||
|
Store This Location for Nearby Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="searchBox"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="forgetSearchBox"
|
||||||
|
>
|
||||||
|
Delete Stored Location
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="resetLatLong"
|
||||||
|
>
|
||||||
|
Reset Marker
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="cancelSearchBoxSelect"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Animation -->
|
||||||
|
<div
|
||||||
|
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||||
|
v-if="isLoading"
|
||||||
|
>
|
||||||
|
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul class="">
|
<InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox">
|
||||||
<li class="border-b border-slate-300">
|
<ul>
|
||||||
<a href="project-view.html" class="block py-4 flex gap-4">
|
<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="w-12">
|
<div class="w-12">
|
||||||
<img
|
<EntityIcon
|
||||||
src="https://picsum.photos/200/200?random=1"
|
:entityId="project.handleId"
|
||||||
class="w-full rounded"
|
:iconSize="48"
|
||||||
/>
|
class="block border border-slate-300 rounded-md"
|
||||||
|
></EntityIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<h2 class="text-base font-semibold">Canyon cleanup</h2>
|
<h2 class="text-base font-semibold">{{ project.name }}</h2>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa> Rotary
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
</div>
|
{{
|
||||||
</div>
|
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
|
||||||
</a>
|
}}
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="border-b border-slate-300">
|
|
||||||
<a href="project-view.html" class="block py-4 flex gap-4">
|
|
||||||
<div class="w-12">
|
|
||||||
<img
|
|
||||||
src="https://picsum.photos/200/200?random=2"
|
|
||||||
class="w-full rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grow">
|
|
||||||
<h2 class="text-base font-semibold">Potluck with neighbors</h2>
|
|
||||||
<div class="text-sm">
|
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa> Andrew A.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="border-b border-slate-300">
|
|
||||||
<a href="project-view.html" class="block py-4 flex gap-4">
|
|
||||||
<div class="w-12">
|
|
||||||
<img
|
|
||||||
src="https://picsum.photos/200/200?random=3"
|
|
||||||
class="w-full rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grow">
|
|
||||||
<h2 class="text-base font-semibold">Historical site</h2>
|
|
||||||
<div class="text-sm">
|
|
||||||
<fa icon="user" class="fa-fw text-slate-400 mr-1"></fa>
|
|
||||||
<em>Unknown</em>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</InfiniteScroll>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isLocalActive && isChoosingSearchBox"
|
||||||
|
style="height: 600px; width: 800px"
|
||||||
|
>
|
||||||
|
<l-map
|
||||||
|
ref="map"
|
||||||
|
:center="[localCenterLat, localCenterLong]"
|
||||||
|
v-model:zoom="localZoom"
|
||||||
|
@click="setMapPoint"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
:lat-lng="[localCenterLat, localCenterLong]"
|
||||||
|
@click="isNewMarkerSet = false"
|
||||||
|
/>
|
||||||
|
<l-rectangle
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
:bounds="[
|
||||||
|
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
||||||
|
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
||||||
|
]"
|
||||||
|
:weight="1"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { LeafletMouseEvent } from "leaflet";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import {
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LRectangle,
|
||||||
|
LTileLayer,
|
||||||
|
} from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
@Options({
|
import { accountsDB, db } from "@/db/index";
|
||||||
components: {},
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import { didInfo, ProjectData } from "@/libs/endorserServer";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
|
||||||
|
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||||
|
const WORLD_ZOOM = 2;
|
||||||
|
const DEFAULT_ZOOM = 2;
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
LRectangle,
|
||||||
|
QuickNav,
|
||||||
|
InfiniteScroll,
|
||||||
|
EntityIcon,
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LTileLayer,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {}
|
export default class DiscoverView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
activeDid = "";
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
|
apiServer = "";
|
||||||
|
searchTerms = "";
|
||||||
|
projects: ProjectData[] = [];
|
||||||
|
isChoosingSearchBox = false;
|
||||||
|
isLocalActive = true;
|
||||||
|
isRemoteActive = false;
|
||||||
|
isNewMarkerSet = false;
|
||||||
|
localCenterLat = 0;
|
||||||
|
localCenterLong = 0;
|
||||||
|
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localCount = 0;
|
||||||
|
localZoom = DEFAULT_ZOOM;
|
||||||
|
remoteCount = 0;
|
||||||
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
// make this function available to the Vue template
|
||||||
|
didInfo = didInfo;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
|
this.resetLatLong();
|
||||||
|
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
|
this.searchLocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async buildHeaders(): Promise<HeadersInit> {
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = allAccounts.find((acc) => acc.did === this.activeDid);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers["Authorization"] = "Bearer " + (await accessToken(identity));
|
||||||
|
} else {
|
||||||
|
// it's OK without auth... we just won't get any identifiers
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async searchAll(beforeId?: string) {
|
||||||
|
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
|
||||||
|
|
||||||
|
if (beforeId) {
|
||||||
|
queryParams = queryParams + `&beforeId=${beforeId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
const response = await fetch(
|
||||||
|
this.apiServer + "/api/v2/report/plans?" + queryParams,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await this.buildHeaders(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
const details = await response.text();
|
||||||
|
console.log("Problem with full search:", details);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: `There was a problem accessing the server. Try again later.`,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw details;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
|
||||||
|
const plans: ProjectData[] = results.data;
|
||||||
|
if (plans) {
|
||||||
|
for (const plan of plans) {
|
||||||
|
const { name, description, handleId, rowid } = plan;
|
||||||
|
this.projects.push({ name, description, handleId, rowid });
|
||||||
|
}
|
||||||
|
this.remoteCount = this.projects.length;
|
||||||
|
} else {
|
||||||
|
throw JSON.stringify(results);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Error with feed load:", e);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: e.userMessage || "There was a problem retrieving projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async searchLocal(beforeId?: string) {
|
||||||
|
if (!this.searchBox) {
|
||||||
|
this.projects = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimContents =
|
||||||
|
"claimContents=" + encodeURIComponent(this.searchTerms);
|
||||||
|
let queryParams = [
|
||||||
|
claimContents,
|
||||||
|
"minLocLat=" + this.searchBox.bbox.minLat,
|
||||||
|
"maxLocLat=" + this.searchBox.bbox.maxLat,
|
||||||
|
"westLocLon=" + this.searchBox.bbox.westLong,
|
||||||
|
"eastLocLon=" + this.searchBox.bbox.eastLong,
|
||||||
|
].join("&");
|
||||||
|
|
||||||
|
if (beforeId) {
|
||||||
|
queryParams = queryParams + `&beforeId=${beforeId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
const response = await fetch(
|
||||||
|
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await this.buildHeaders(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
const details = await response.text();
|
||||||
|
console.log("Problem with nearby search:", details);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "There was a problem accessing the server. Try again later.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
throw await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
|
||||||
|
if (results.data) {
|
||||||
|
if (beforeId) {
|
||||||
|
const plans: ProjectData[] = results.data;
|
||||||
|
for (const plan of plans) {
|
||||||
|
const { name, description, handleId = plan.handleId, rowid } = plan;
|
||||||
|
if (beforeId !== plan["rowid"]) {
|
||||||
|
this.projects.push({ name, description, handleId, rowid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.projects = results.data;
|
||||||
|
}
|
||||||
|
this.localCount = this.projects.length;
|
||||||
|
} else {
|
||||||
|
throw JSON.stringify(results);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Error with feed load:", e);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: e.userMessage || "There was a problem retrieving projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data loader used by infinite scroller
|
||||||
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
|
**/
|
||||||
|
async loadMoreData(payload: boolean) {
|
||||||
|
if (this.projects.length > 0 && payload) {
|
||||||
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
|
if (this.isLocalActive) {
|
||||||
|
this.searchLocal(latestProject["rowid"]);
|
||||||
|
} else if (this.isRemoteActive) {
|
||||||
|
this.searchAll(latestProject["rowid"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle clicking on a project entry found in the list
|
||||||
|
* @param id of the project
|
||||||
|
**/
|
||||||
|
onClickLoadProject(id: string) {
|
||||||
|
localStorage.setItem("projectId", id);
|
||||||
|
const route = {
|
||||||
|
name: "project",
|
||||||
|
};
|
||||||
|
this.$router.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMapPoint(event: LeafletMouseEvent) {
|
||||||
|
if (this.isNewMarkerSet) {
|
||||||
|
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
||||||
|
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
||||||
|
} else {
|
||||||
|
// marker is not set
|
||||||
|
this.localCenterLat = event.latlng.lat;
|
||||||
|
this.localCenterLong = event.latlng.lng;
|
||||||
|
|
||||||
|
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
// Guess at a size for the bounding box.
|
||||||
|
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
||||||
|
const bounds = event.target.boxZoom?._map?.getBounds();
|
||||||
|
if (bounds) {
|
||||||
|
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
||||||
|
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
||||||
|
}
|
||||||
|
this.localLatDiff = latDiff;
|
||||||
|
this.localLongDiff = longDiff;
|
||||||
|
this.isNewMarkerSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetLatLong() {
|
||||||
|
if (this.searchBox?.bbox) {
|
||||||
|
const bbox = this.searchBox.bbox;
|
||||||
|
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
||||||
|
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
||||||
|
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
||||||
|
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
||||||
|
this.localZoom = WORLD_ZOOM;
|
||||||
|
this.isNewMarkerSet = true;
|
||||||
|
} else {
|
||||||
|
this.isNewMarkerSet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async storeSearchBox() {
|
||||||
|
if (this.localCenterLong || this.localCenterLat) {
|
||||||
|
try {
|
||||||
|
const newSearchBox = {
|
||||||
|
name: "Local",
|
||||||
|
bbox: {
|
||||||
|
eastLong: this.localCenterLong + this.localLongDiff,
|
||||||
|
maxLat: this.localCenterLat + this.localLatDiff,
|
||||||
|
minLat: this.localCenterLat - this.localLatDiff,
|
||||||
|
westLong: this.localCenterLong - this.localLongDiff,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
searchBoxes: [newSearchBox],
|
||||||
|
});
|
||||||
|
this.searchBox = newSearchBox;
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.searchLocal();
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Search Settings",
|
||||||
|
text: "Try going to a different page and then coming back.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to retry the location search setting because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "No Location Selected",
|
||||||
|
text: "Select a location on the map.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async forgetSearchBox() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
searchBoxes: [],
|
||||||
|
});
|
||||||
|
this.searchBox = null;
|
||||||
|
this.localCenterLat = 0;
|
||||||
|
this.localCenterLong = 0;
|
||||||
|
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
this.localZoom = DEFAULT_ZOOM;
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.isNewMarkerSet = false;
|
||||||
|
this.searchLocal();
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Search Settings",
|
||||||
|
text: "Try going to a different page and then coming back.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to retry the location search setting because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelSearchBoxSelect() {
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.localZoom = WORLD_ZOOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedLocalTabClassNames() {
|
||||||
|
return {
|
||||||
|
"inline-block": true,
|
||||||
|
"py-3": true,
|
||||||
|
"rounded-t-lg": true,
|
||||||
|
"border-b-2": true,
|
||||||
|
active: this.isLocalActive,
|
||||||
|
"text-blue-600": this.isLocalActive,
|
||||||
|
"border-blue-600": this.isLocalActive,
|
||||||
|
"font-semibold": this.isLocalActive,
|
||||||
|
"border-transparent": !this.isLocalActive,
|
||||||
|
"hover:text-slate-600": !this.isLocalActive,
|
||||||
|
"hover:border-slate-300": !this.isLocalActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedRemoteTabClassNames() {
|
||||||
|
return {
|
||||||
|
"inline-block": true,
|
||||||
|
"py-3": true,
|
||||||
|
"rounded-t-lg": true,
|
||||||
|
"border-b-2": true,
|
||||||
|
active: this.isRemoteActive,
|
||||||
|
"text-blue-600": this.isRemoteActive,
|
||||||
|
"border-blue-600": this.isRemoteActive,
|
||||||
|
"font-semibold": this.isRemoteActive,
|
||||||
|
"border-transparent": !this.isRemoteActive,
|
||||||
|
"hover:text-slate-600": !this.isRemoteActive,
|
||||||
|
"hover:border-slate-300": !this.isRemoteActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,52 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<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 -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
@@ -60,14 +13,14 @@
|
|||||||
gifts and collaboration.
|
gifts and collaboration.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How is this app useful?</h2>
|
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
|
||||||
<p>
|
<p>
|
||||||
We are building networks of people who want to grow a gifting society.
|
We are building networks of people who want to grow a gifting society.
|
||||||
First of all, you can record ways you've seen people give, and that
|
First of all, you can record ways you've seen people give, and that
|
||||||
leaves a permanent record... one that they show came from you. This is
|
leaves a permanent record -- one that came from you, and the recipient
|
||||||
personally gratifying, but it extends to broader work: volunteers can
|
can prove it was for them. This is personally gratifying, but it extends
|
||||||
get confirmation of activity and selectively show off their
|
to broader work: volunteers can get confirmation of activity and
|
||||||
contributions and network.
|
selectively show off their contributions and network.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can also record projects and plans and invite others to collaborate.
|
You can also record projects and plans and invite others to collaborate.
|
||||||
@@ -83,10 +36,25 @@
|
|||||||
the control; this app gives you the control.
|
the control; this app gives you the control.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">How do I take my first action?</h2>
|
||||||
|
<p>
|
||||||
|
You need someone to register you -- usually the person who told you
|
||||||
|
about this app, on the Contacts
|
||||||
|
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
|
||||||
|
and after you have contacts, you can select any contact on the home page
|
||||||
|
and record your appreciation for... whatever. That is a claim recorded
|
||||||
|
on a custom ledger. The day after being registered, you'll be able to
|
||||||
|
register others; later, you can create projects, too.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Note that there are limits to how many each person can register, so you
|
||||||
|
may have to wait.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
|
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
|
||||||
<p>
|
<p>
|
||||||
There are two parts to backup your data: the identifier secrets and the
|
There are two sets of data to backup: the identifier secrets and the
|
||||||
other data such as settings, contacts, etc.
|
other data that isn't quite a secret such as settings, contacts, etc.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
@@ -100,6 +68,10 @@
|
|||||||
<li>
|
<li>
|
||||||
Click on "Backup Identifier Seed" and follow the instructions.
|
Click on "Backup Identifier Seed" and follow the instructions.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
If you have other identifiers, switch to each one and repeat those
|
||||||
|
steps.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
@@ -156,26 +128,12 @@
|
|||||||
key.
|
key.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
||||||
How do I get permission to store claims on the server?
|
|
||||||
</h2>
|
|
||||||
<p>
|
<p>
|
||||||
Get registered by someone else with the app; they can register you on
|
Go
|
||||||
the Contacts <fa icon="circle-user" class="fa-fw" /> page. There are
|
<router-link to="start" class="text-blue-500">
|
||||||
limits to how many each person can register, so you may have to wait.
|
create another identity here.
|
||||||
</p>
|
</router-link>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What do you mean by "claims"?</h2>
|
|
||||||
<p>
|
|
||||||
Certain actions in this app are signed by your private keys, and these
|
|
||||||
are often called "claims". For example, when you give time to a person
|
|
||||||
or project, you sign a claim declaring that you gave that time. When you
|
|
||||||
declare a project, you sign a claim declaring it to the world. When you
|
|
||||||
confirm someone else's claim, you sign a claim of agreement.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Some of the data in this app does not involve claims, such as your
|
|
||||||
contact list and your identifier.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
@@ -196,14 +154,6 @@
|
|||||||
page.
|
page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
|
||||||
<p>
|
|
||||||
Go
|
|
||||||
<router-link to="import-account" class="text-blue-500">
|
|
||||||
import another mnemonic here.
|
|
||||||
</router-link>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
||||||
<p>
|
<p>
|
||||||
See
|
See
|
||||||
@@ -216,6 +166,14 @@
|
|||||||
<p>
|
<p>
|
||||||
{{ package.version }}
|
{{ package.version }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
For any other questions, including remove your data:
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Contact us through
|
||||||
|
<a href="https://communitycred.org">CommunityCred.org</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -223,8 +181,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Package from "../../package.json";
|
import * as Package from "../../package.json";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
|
||||||
@Component
|
@Component({ components: { QuickNav } })
|
||||||
export default class Help extends Vue {
|
export default class Help extends Vue {
|
||||||
package = Package;
|
package = Package;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,580 @@
|
|||||||
<template>
|
<template>
|
||||||
<section></section>
|
<QuickNav selected="Home"></QuickNav>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Time Safari
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'toast',
|
||||||
|
text: 'I\'m a toast. Don\'t mind me.',
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Toast (self-dismiss)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'info',
|
||||||
|
title: 'Information Alert',
|
||||||
|
text: 'Just wanted you to know.',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'success',
|
||||||
|
title: 'Success Alert',
|
||||||
|
text: 'Congratulations!',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Success
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Warning Alert',
|
||||||
|
text: 'You might wanna look at this.',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Warning
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'danger',
|
||||||
|
title: 'Danger Alert',
|
||||||
|
text: 'Something terrible has happened!',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Danger
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'modal',
|
||||||
|
type: 'notification-permission',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Notif ON
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'modal',
|
||||||
|
type: 'notification-mute',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Notif MUTE
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'modal',
|
||||||
|
type: 'notification-off',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Notif OFF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold">Quick Action</h2>
|
||||||
|
<p class="mb-4">Record a gift from a contact:</p>
|
||||||
|
|
||||||
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
|
<li @click="openDialog()">
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="null"
|
||||||
|
:iconSize="64"
|
||||||
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
|
></EntityIcon>
|
||||||
|
<h3
|
||||||
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
Anonymous
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="contact in allContacts"
|
||||||
|
:key="contact.did"
|
||||||
|
@click="openDialog(contact)"
|
||||||
|
>
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="64"
|
||||||
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
|
></EntityIcon>
|
||||||
|
<h3
|
||||||
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
{{ contact.name || contact.did }}
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||||
|
<router-link
|
||||||
|
v-if="allContacts.length > 7"
|
||||||
|
:to="{ name: 'contact-gives' }"
|
||||||
|
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Show More Contacts…
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<!-- If there are no contacts, show this instead: -->
|
||||||
|
<div
|
||||||
|
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
|
||||||
|
v-if="allContacts.length === 0"
|
||||||
|
>
|
||||||
|
(No contacts to show.)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GiftedDialog
|
||||||
|
ref="customDialog"
|
||||||
|
@dialog-result="handleDialogResult"
|
||||||
|
message="Received from"
|
||||||
|
>
|
||||||
|
</GiftedDialog>
|
||||||
|
|
||||||
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
||||||
|
<div :class="{ hidden: isHiddenSpinner }">
|
||||||
|
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||||
|
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul class="border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
class="border-b border-slate-300 py-2"
|
||||||
|
v-for="record in feedData"
|
||||||
|
:key="record.jwtId"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
|
||||||
|
v-if="record.jwtId == feedLastViewedId"
|
||||||
|
>
|
||||||
|
You've seen all claims below:
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
|
||||||
|
<!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
|
||||||
|
<span class="">{{ this.giveDescription(record) }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import { db, accountsDB } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
|
didInfo,
|
||||||
|
GiverInputInfo,
|
||||||
|
GiverOutputInfo,
|
||||||
|
GiveServerRecord,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
|
||||||
@Options({
|
interface Notification {
|
||||||
components: {
|
group: string;
|
||||||
HelloWorld,
|
type: string;
|
||||||
},
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {}
|
export default class HomeView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
activeDid = "";
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
|
apiServer = "";
|
||||||
|
feedAllLoaded = false;
|
||||||
|
feedData = [];
|
||||||
|
feedPreviousOldestId?: string;
|
||||||
|
feedLastViewedId?: string;
|
||||||
|
isHiddenSpinner = true;
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity: IIdentifier) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||||
|
this.updateAllFeed();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
err.userMessage ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async buildHeaders() {
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = allAccounts.find((acc) => acc.did === this.activeDid);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers["Authorization"] = "Bearer " + (await accessToken(identity));
|
||||||
|
} else {
|
||||||
|
// it's OK without auth... we just won't get any identifiers
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateAllFeed() {
|
||||||
|
this.isHiddenSpinner = false;
|
||||||
|
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
|
||||||
|
.then(async (results) => {
|
||||||
|
if (results.data.length > 0) {
|
||||||
|
this.feedData = this.feedData.concat(results.data);
|
||||||
|
this.feedAllLoaded = results.hitLimit;
|
||||||
|
this.feedPreviousOldestId =
|
||||||
|
results.data[results.data.length - 1].jwtId;
|
||||||
|
if (
|
||||||
|
this.feedLastViewedId == null ||
|
||||||
|
this.feedLastViewedId < results.data[0].jwtId
|
||||||
|
) {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
lastViewedClaimId: results.data[0].jwtId,
|
||||||
|
});
|
||||||
|
// but not for this page because we need to remember what it was before
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("Error with feed load:", e);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Export Error",
|
||||||
|
text: e.userMessage || "There was an error retrieving feed data.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isHiddenSpinner = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
|
||||||
|
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
||||||
|
const response = await fetch(
|
||||||
|
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await this.buildHeaders(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
|
||||||
|
if (results.data) {
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
throw JSON.stringify(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
giveDescription(giveRecord: GiveServerRecord) {
|
||||||
|
// claim.claim happen for some claims wrapped in a Verifiable Credential
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
|
||||||
|
// agent.did is for legacy data, before March 2023
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did;
|
||||||
|
const giverInfo = didInfo(
|
||||||
|
giverDid,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
);
|
||||||
|
const gaveAmount = claim.object?.amountOfThisGood
|
||||||
|
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||||
|
: claim.description || "something unknown";
|
||||||
|
// recipient.did is for legacy data, before March 2023
|
||||||
|
const gaveRecipientId =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
claim.recipient?.identifier || (claim.recipient as any)?.did;
|
||||||
|
const gaveRecipientInfo = gaveRecipientId
|
||||||
|
? " to " +
|
||||||
|
didInfo(
|
||||||
|
gaveRecipientId,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
)
|
||||||
|
: "";
|
||||||
|
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayAmount(code: string, amt: number) {
|
||||||
|
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
currencyShortWordForCode(unitCode: string, single: boolean) {
|
||||||
|
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
openDialog(giver: GiverInputInfo) {
|
||||||
|
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDialogResult(result: GiverOutputInfo) {
|
||||||
|
if (result.action === "confirm") {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.recordGive(
|
||||||
|
result.giver?.did,
|
||||||
|
result.description,
|
||||||
|
result.hours,
|
||||||
|
).then(() => {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// action was "cancel" so do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param giverDid may be null
|
||||||
|
* @param description may be an empty string
|
||||||
|
* @param hours may be 0
|
||||||
|
*/
|
||||||
|
public async recordGive(
|
||||||
|
giverDid?: string,
|
||||||
|
description?: string,
|
||||||
|
hours?: number,
|
||||||
|
) {
|
||||||
|
if (!this.activeDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identity before you can record a give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description && !hours) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must enter a description or some number of hours.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
const result = await createAndSubmitGive(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
identity,
|
||||||
|
giverDid,
|
||||||
|
this.activeDid,
|
||||||
|
description,
|
||||||
|
hours,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.isGiveCreationError(result)) {
|
||||||
|
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||||
|
console.log("Error with give result:", result);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage || "There was an error recording the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: "That gift was recorded.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("Error with give caught:", error);
|
||||||
|
const message =
|
||||||
|
error.userMessage ||
|
||||||
|
error.response?.data?.error?.message ||
|
||||||
|
"There was an error recording the give.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for readability
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
isGiveCreationError(result: any) {
|
||||||
|
return result.status !== 201 || result.data?.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
getGiveCreationErrorMessage(result: any) {
|
||||||
|
return result.data?.error?.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
168
src/views/IdentitySwitcherView.vue
Normal file
168
src/views/IdentitySwitcherView.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
<!-- Cancel -->
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'account' }"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
><fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
Switch Identity
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Identity List -->
|
||||||
|
|
||||||
|
<!-- Current Identity - Display First! -->
|
||||||
|
<div class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4">
|
||||||
|
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
||||||
|
<span class="overflow-hidden">
|
||||||
|
<h2 class="text-xl font-semibold mb-0">
|
||||||
|
{{ firstName }} {{ lastName }}
|
||||||
|
</h2>
|
||||||
|
<div class="text-sm text-slate-500 truncate">
|
||||||
|
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Other Identity/ies -->
|
||||||
|
<ul class="mb-4">
|
||||||
|
<li
|
||||||
|
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
|
||||||
|
v-for="ident in otherIdentities"
|
||||||
|
:key="ident.did"
|
||||||
|
@click="switchAccount(ident.did)"
|
||||||
|
>
|
||||||
|
<fa icon="circle" class="fa-fw text-slate-400 text-xl mr-3"></fa>
|
||||||
|
<span class="overflow-hidden">
|
||||||
|
<h2 class="text-xl font-semibold mb-0"></h2>
|
||||||
|
<div class="text-sm text-slate-500 truncate">
|
||||||
|
<b>ID:</b> <code>{{ ident.did }}</code>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'start' }"
|
||||||
|
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Add Another Identity…
|
||||||
|
</router-link>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
|
||||||
|
@click="switchAccount('0')"
|
||||||
|
>
|
||||||
|
No Identity
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { AppString } from "@/constants/app";
|
||||||
|
import { db, accountsDB } from "@/db/index";
|
||||||
|
import { AccountsSchema } from "@/db/tables/accounts";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({ components: { QuickNav } })
|
||||||
|
export default class IdentitySwitcherView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
Constants = AppString;
|
||||||
|
public accounts: typeof AccountsSchema;
|
||||||
|
public activeDid = "";
|
||||||
|
public apiServer = "";
|
||||||
|
public apiServerInput = "";
|
||||||
|
public firstName = "";
|
||||||
|
public lastName = "";
|
||||||
|
public otherIdentities: Array<{ did: string }> = [];
|
||||||
|
public showContactGives = false;
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.apiServerInput = settings?.apiServer || "";
|
||||||
|
this.firstName = settings?.firstName || "No";
|
||||||
|
this.lastName = settings?.lastName || "Name";
|
||||||
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
|
||||||
|
if (identity) {
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: identity.did,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
for (let n = 0; n < accounts.length; n++) {
|
||||||
|
const did = JSON.parse(accounts[n].identity)["did"];
|
||||||
|
if (did && this.activeDid !== did) {
|
||||||
|
this.otherIdentities.push({ did: did });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Loading Accounts",
|
||||||
|
text: "Clear your cache and start over (after data backup).",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error("Telling user to clear cache at page create because:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchAccount(did?: string) {
|
||||||
|
// 0 means none
|
||||||
|
if (did === "0") {
|
||||||
|
did = undefined;
|
||||||
|
}
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: did,
|
||||||
|
});
|
||||||
|
this.activeDid = did || "";
|
||||||
|
this.otherIdentities = [];
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
for (let n = 0; n < accounts.length; n++) {
|
||||||
|
const did = JSON.parse(accounts[n].identity)["did"];
|
||||||
|
if (did && this.activeDid !== did) {
|
||||||
|
this.otherIdentities.push({ did: did });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$router.push({ name: "account" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -24,6 +24,24 @@
|
|||||||
v-model="mnemonic"
|
v-model="mnemonic"
|
||||||
/>
|
/>
|
||||||
{{ mnemonic }}
|
{{ mnemonic }}
|
||||||
|
<h3
|
||||||
|
class="text-sm uppercase font-semibold mb-3"
|
||||||
|
@click="showAdvanced = !showAdvanced"
|
||||||
|
>
|
||||||
|
Advanced
|
||||||
|
</h3>
|
||||||
|
<div v-if="showAdvanced">
|
||||||
|
Enter a custom derivation path
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
v-model="derivationPath"
|
||||||
|
/>
|
||||||
|
For previous uPort or Endorser users,
|
||||||
|
<a @click="derivationPath = UPORT_DERIVATION_PATH" class="text-blue-500">
|
||||||
|
click here to use that value.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<button
|
<button
|
||||||
@click="from_mnemonic()"
|
@click="from_mnemonic()"
|
||||||
@@ -43,20 +61,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
import {
|
||||||
import { accountsDB, db } from "@/db";
|
DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
|
deriveAddress,
|
||||||
|
newIdentifier,
|
||||||
|
} from "../libs/crypto";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ImportAccountView extends Vue {
|
export default class ImportAccountView extends Vue {
|
||||||
|
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
|
||||||
|
|
||||||
mnemonic = "";
|
mnemonic = "";
|
||||||
address = "";
|
address = "";
|
||||||
privateHex = "";
|
privateHex = "";
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
derivationPath = "";
|
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
|
||||||
|
showAdvanced = false;
|
||||||
|
|
||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
@@ -65,14 +90,16 @@ export default class ImportAccountView extends Vue {
|
|||||||
public async from_mnemonic() {
|
public async from_mnemonic() {
|
||||||
const mne: string = this.mnemonic.trim().toLowerCase();
|
const mne: string = this.mnemonic.trim().toLowerCase();
|
||||||
if (this.mnemonic.trim().length > 0) {
|
if (this.mnemonic.trim().length > 0) {
|
||||||
[this.address, this.privateHex, this.publicHex, this.derivationPath] =
|
[this.address, this.privateHex, this.publicHex] = deriveAddress(
|
||||||
deriveAddress(mne);
|
mne,
|
||||||
|
this.derivationPath,
|
||||||
|
);
|
||||||
|
|
||||||
const newId = newIdentifier(
|
const newId = newIdentifier(
|
||||||
this.address,
|
this.address,
|
||||||
this.publicHex,
|
this.publicHex,
|
||||||
this.privateHex,
|
this.privateHex,
|
||||||
this.derivationPath
|
this.derivationPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
163
src/views/ImportDerivedAccountView.vue
Normal file
163
src/views/ImportDerivedAccountView.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
<!-- Cancel -->
|
||||||
|
<button
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left"></fa>
|
||||||
|
</button>
|
||||||
|
Derive from Existing Identity
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<!-- Import Account Form -->
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-center text-xl mb-4 font-light">
|
||||||
|
Will increment the maximum derivation path from the existing seed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-if="didArrays.length > 1">
|
||||||
|
Choose existing DIDs from same seed phrase to compute derivation.
|
||||||
|
</p>
|
||||||
|
<ul class="mb-4">
|
||||||
|
<li
|
||||||
|
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
|
||||||
|
v-for="dids in didArrays"
|
||||||
|
:key="dids[0]"
|
||||||
|
@click="switchAccount(dids[0])"
|
||||||
|
>
|
||||||
|
<fa
|
||||||
|
v-if="dids[0] == selectedArrayFirstDid"
|
||||||
|
icon="circle"
|
||||||
|
class="fa-fw text-blue-400 text-xl mr-3"
|
||||||
|
></fa>
|
||||||
|
<fa
|
||||||
|
v-else
|
||||||
|
icon="circle"
|
||||||
|
class="fa-fw text-slate-400 text-xl mr-3"
|
||||||
|
></fa>
|
||||||
|
<span class="overflow-hidden">
|
||||||
|
<div class="text-sm text-slate-500 truncate">
|
||||||
|
<code>{{ dids.join(",") }}</code>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8">
|
||||||
|
<button
|
||||||
|
@click="incrementDerivation()"
|
||||||
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Increment and Import
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="onCancelClick()"
|
||||||
|
type="button"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import {
|
||||||
|
DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
|
deriveAddress,
|
||||||
|
newIdentifier,
|
||||||
|
} from "../libs/crypto";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {},
|
||||||
|
})
|
||||||
|
export default class ImportAccountView extends Vue {
|
||||||
|
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
|
||||||
|
didArrays: Array<Array<string>> = [];
|
||||||
|
selectedArrayFirstDid = "";
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
const seedDids: Record<string, Array<string>> = {};
|
||||||
|
accounts.forEach((account) => {
|
||||||
|
const prevDids: Array<string> = seedDids[account.mnemonic] || [];
|
||||||
|
seedDids[account.mnemonic] = prevDids.concat([account.did]);
|
||||||
|
});
|
||||||
|
this.didArrays = Object.values(seedDids);
|
||||||
|
this.selectedArrayFirstDid = this.didArrays[0][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCancelClick() {
|
||||||
|
this.$router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public switchAccount(did: string) {
|
||||||
|
this.selectedArrayFirstDid = did;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async incrementDerivation() {
|
||||||
|
await accountsDB.open();
|
||||||
|
// find the maximum derivation path for the selected DIDs
|
||||||
|
const selectedArray: Array<string> =
|
||||||
|
this.didArrays.find((dids) => dids[0] === this.selectedArrayFirstDid) ||
|
||||||
|
[];
|
||||||
|
const allMatchingAccounts = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.anyOf(...selectedArray)
|
||||||
|
.toArray();
|
||||||
|
const accountWithMaxDeriv = allMatchingAccounts[0];
|
||||||
|
allMatchingAccounts.slice(1).forEach((account) => {
|
||||||
|
if (account.derivationPath > accountWithMaxDeriv.derivationPath) {
|
||||||
|
accountWithMaxDeriv.derivationPath = account.derivationPath;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// increment the last number in that max derivation path
|
||||||
|
let lastStr = accountWithMaxDeriv.derivationPath.split("/").slice(-1)[0];
|
||||||
|
if (lastStr.endsWith("'")) {
|
||||||
|
lastStr = lastStr.slice(0, -1);
|
||||||
|
}
|
||||||
|
const lastNum = parseInt(lastStr, 10);
|
||||||
|
const newLastNum = lastNum + 1;
|
||||||
|
const newDerivPath = accountWithMaxDeriv.derivationPath
|
||||||
|
.split("/")
|
||||||
|
.slice(0, -1)
|
||||||
|
.concat([newLastNum.toString() + "'"])
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
const mne: string = accountWithMaxDeriv.mnemonic;
|
||||||
|
|
||||||
|
const [address, privateHex, publicHex] = deriveAddress(mne, newDerivPath);
|
||||||
|
|
||||||
|
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await accountsDB.accounts.add({
|
||||||
|
dateCreated: new Date().toISOString(),
|
||||||
|
derivationPath: newDerivPath,
|
||||||
|
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.error("Error saving mnemonic & updating settings:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
[New/Edit] Identity
|
[New/Edit] Identity
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<form>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="First Name"
|
placeholder="First Name"
|
||||||
@@ -44,16 +44,15 @@
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class NewEditAccountView extends Vue {
|
export default class NewEditAccountView extends Vue {
|
||||||
|
|||||||
@@ -16,10 +16,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Project Details -->
|
<!-- Project Details -->
|
||||||
<form>
|
|
||||||
<select
|
<select 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"
|
|
||||||
>
|
|
||||||
<option disabled>Choose a commitment type…</option>
|
<option disabled>Choose a commitment type…</option>
|
||||||
<option selected>Time</option>
|
<option selected>Time</option>
|
||||||
<option>Cryptocurrency</option>
|
<option>Cryptocurrency</option>
|
||||||
@@ -56,14 +54,13 @@
|
|||||||
Maybe Later
|
Maybe Later
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class NewEditCommitmentView extends Vue {}
|
export default class NewEditCommitmentView extends Vue {}
|
||||||
|
|||||||
@@ -39,6 +39,40 @@
|
|||||||
{{ description.length }}/500 max. characters
|
{{ description.length }}/500 max. characters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="mr-2"
|
||||||
|
v-model="includeLocation"
|
||||||
|
@change="includeLocation = true"
|
||||||
|
/>
|
||||||
|
<label for="includeLocation">Include Location</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
||||||
|
<l-map
|
||||||
|
ref="map"
|
||||||
|
v-model:zoom="zoom"
|
||||||
|
:center="[0, 0]"
|
||||||
|
@click="
|
||||||
|
(event) => {
|
||||||
|
latitude = event.latlng.lat;
|
||||||
|
longitude = event.latlng.lng;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="latitude || longitude"
|
||||||
|
:lat-lng="[latitude, longitude]"
|
||||||
|
@click="maybeEraseLatLong()"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<button
|
<button
|
||||||
:disabled="isHiddenSave"
|
:disabled="isHiddenSave"
|
||||||
@@ -49,8 +83,9 @@
|
|||||||
<span :class="{ hidden: isHiddenSave }">Save Project</span>
|
<span :class="{ hidden: isHiddenSave }">Save Project</span>
|
||||||
|
|
||||||
<!-- SHOW if in saving state; DISABLE button while in saving state -->
|
<!-- SHOW if in saving state; DISABLE button while in saving state -->
|
||||||
<span :class="{ hidden: isHiddenSpinner }"
|
<span :class="{ hidden: isHiddenSpinner }">
|
||||||
><i class="fa-solid fa-spinner fa-spin-pulse"></i>
|
<!-- icon no worky? -->
|
||||||
|
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
|
||||||
Saving…</span
|
Saving…</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
@@ -63,55 +98,80 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- This same popup code is in many files. -->
|
|
||||||
<div v-bind:class="computedAlertClassNames()">
|
|
||||||
<button
|
|
||||||
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
|
||||||
@click="onClickClose()"
|
|
||||||
>
|
|
||||||
<fa icon="xmark"></fa>
|
|
||||||
</button>
|
|
||||||
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
|
||||||
<p>{{ alertMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import * as R from "ramda";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||||
import { useAppStore } from "@/store/app";
|
import { useAppStore } from "@/store/app";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
import { PlanVerifiableCredential } from "@/libs/endorserServer";
|
||||||
|
|
||||||
interface VerifiableCredential {
|
interface Notification {
|
||||||
"@context": string;
|
group: string;
|
||||||
"@type": string;
|
type: string;
|
||||||
name: string;
|
title: string;
|
||||||
description: string;
|
text: string;
|
||||||
identifier?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: { LMap, LMarker, LTileLayer },
|
||||||
})
|
})
|
||||||
export default class NewEditProjectView extends Vue {
|
export default class NewEditProjectView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
projectName = "";
|
|
||||||
description = "";
|
description = "";
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
includeLocation = false;
|
||||||
|
latitude = 0;
|
||||||
|
longitude = 0;
|
||||||
|
numAccounts = 0;
|
||||||
|
projectName = "";
|
||||||
|
zoom = 2;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load project records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity: IIdentifier) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
projectId = localStorage.getItem("projectId") || "";
|
projectId = localStorage.getItem("projectId") || "";
|
||||||
isHiddenSave = false;
|
isHiddenSave = false;
|
||||||
isHiddenSpinner = true;
|
isHiddenSpinner = true;
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
@@ -119,14 +179,15 @@ export default class NewEditProjectView extends Vue {
|
|||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
|
||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
await accountsDB.open();
|
if (this.numAccounts === 0) {
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
console.error("Error: no account was found.");
|
||||||
if (num_accounts === 0) {
|
|
||||||
console.error("Problem! Should have a profile!");
|
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
if (!identity) {
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
throw new Error(
|
||||||
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
||||||
|
);
|
||||||
|
}
|
||||||
this.LoadProject(identity);
|
this.LoadProject(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +218,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
|
|
||||||
private async SaveProject(identity: IIdentifier) {
|
private async SaveProject(identity: IIdentifier) {
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim: VerifiableCredential = {
|
const vcClaim: PlanVerifiableCredential = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "PlanAction",
|
"@type": "PlanAction",
|
||||||
name: this.projectName,
|
name: this.projectName,
|
||||||
@@ -167,6 +228,15 @@ export default class NewEditProjectView extends Vue {
|
|||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
vcClaim.identifier = this.projectId;
|
vcClaim.identifier = this.projectId;
|
||||||
}
|
}
|
||||||
|
if (this.includeLocation) {
|
||||||
|
vcClaim.location = {
|
||||||
|
geo: {
|
||||||
|
"@type": "GeoCoordinates",
|
||||||
|
latitude: this.latitude,
|
||||||
|
longitude: this.longitude,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
@@ -200,47 +270,66 @@ export default class NewEditProjectView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
// handleId is new in server v release-1.6.0; remove fullIri when that
|
||||||
// version shows up here: https://endorser.ch:3000/api-docs/
|
// version shows up here: https://api.endorser.ch/api-docs/
|
||||||
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
||||||
this.errorMessage = "";
|
this.errorMessage = "";
|
||||||
this.alertTitle = "";
|
|
||||||
this.alertMessage = "";
|
|
||||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
// handleId is new in server v release-1.6.0; remove fullIri when that
|
||||||
// version shows up here: https://endorser.ch:3000/api-docs/
|
// version shows up here: https://api.endorser.ch/api-docs/
|
||||||
useAppStore().setProjectId(
|
useAppStore().setProjectId(
|
||||||
resp.data.success.handleId || resp.data.success.fullIri
|
resp.data.success.handleId || resp.data.success.fullIri,
|
||||||
);
|
);
|
||||||
setTimeout(
|
setTimeout(
|
||||||
function (that: Vue) {
|
function (that: NewEditProjectView) {
|
||||||
const route = {
|
that.$router.push({ name: "project" });
|
||||||
name: "project",
|
|
||||||
};
|
|
||||||
that.$router.push(route);
|
|
||||||
},
|
},
|
||||||
2000,
|
2000,
|
||||||
this
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let userMessage = "There was an error. See logs for more info.";
|
let userMessage = "There was an error saving the project.";
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError<{
|
||||||
|
error?: { message?: string };
|
||||||
|
}>;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
this.isAlertVisible = true;
|
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
||||||
if (serverError.message) {
|
console.log(serverError);
|
||||||
this.alertTitle = "User Message";
|
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
|
||||||
userMessage = serverError.message; // This is info for the user.
|
this.$notify(
|
||||||
this.alertMessage = userMessage;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "User Message",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.alertTitle = "Server Message";
|
this.$notify(
|
||||||
this.alertMessage = JSON.stringify(serverError.toJSON());
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Server Message",
|
||||||
|
text: JSON.stringify(serverError.toJSON()),
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
"Here's the full error trying to save the claim:",
|
"Here's the full error trying to save the claim:",
|
||||||
error
|
error,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Claim Error",
|
||||||
|
text: error as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
);
|
);
|
||||||
this.alertTitle = "Claim Error";
|
|
||||||
this.alertMessage = error as string;
|
|
||||||
}
|
}
|
||||||
// Now set that error for the user to see.
|
// Now set that error for the user to see.
|
||||||
this.errorMessage = userMessage;
|
this.errorMessage = userMessage;
|
||||||
@@ -251,46 +340,25 @@ export default class NewEditProjectView extends Vue {
|
|||||||
public async onSaveProjectClick() {
|
public async onSaveProjectClick() {
|
||||||
this.isHiddenSave = true;
|
this.isHiddenSave = true;
|
||||||
this.isHiddenSpinner = false;
|
this.isHiddenSpinner = false;
|
||||||
await accountsDB.open();
|
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
if (this.numAccounts === 0) {
|
||||||
if (num_accounts === 0) {
|
console.error("Error: there is no account.");
|
||||||
console.error("Problem! Should have a profile!");
|
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
|
||||||
this.SaveProject(identity);
|
this.SaveProject(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public maybeEraseLatLong() {
|
||||||
|
if (window.confirm("Are you sure you don't want to mark a location?")) {
|
||||||
|
this.latitude = 0;
|
||||||
|
this.longitude = 0;
|
||||||
|
this.includeLocation = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
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>
|
</script>
|
||||||
|
|||||||
81
src/views/NewIdentifierView.vue
Normal file
81
src/views/NewIdentifierView.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Your Identity
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="flex justify-center py-12">
|
||||||
|
<span />
|
||||||
|
<span v-if="loading">
|
||||||
|
<span class="text-xl">Creating... </span>
|
||||||
|
<fa
|
||||||
|
icon="spinner"
|
||||||
|
class="fa-spin fa-spin-pulse"
|
||||||
|
color="green"
|
||||||
|
size="128"
|
||||||
|
></fa>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<span class="text-xl">Created!</span>
|
||||||
|
<fa
|
||||||
|
icon="burst"
|
||||||
|
class="fa-beat px-12"
|
||||||
|
color="green"
|
||||||
|
style="
|
||||||
|
--fa-animation-duration: 1s;
|
||||||
|
--fa-animation-direction: reverse;
|
||||||
|
--fa-animation-iteration-count: 1;
|
||||||
|
--fa-beat-scale: 6;
|
||||||
|
"
|
||||||
|
></fa>
|
||||||
|
</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import "dexie-export-import";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
|
||||||
|
@Component({ components: { QuickNav } })
|
||||||
|
export default class AccountViewView extends Vue {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
const mnemonic = generateSeed();
|
||||||
|
// address is 0x... ETH address, without "did:eth:"
|
||||||
|
const [address, privateHex, publicHex, derivationPath] =
|
||||||
|
deriveAddress(mnemonic);
|
||||||
|
|
||||||
|
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||||
|
const identity = JSON.stringify(newId);
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
await accountsDB.accounts.add({
|
||||||
|
dateCreated: new Date().toISOString(),
|
||||||
|
derivationPath: derivationPath,
|
||||||
|
did: newId.did,
|
||||||
|
identity: identity,
|
||||||
|
mnemonic: mnemonic,
|
||||||
|
publicKeyHex: newId.keys[0].publicKeyHex,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: newId.did,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push({ name: "account" });
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,48 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Projects"></QuickNav>
|
||||||
<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 bg-slate-400 text-white">
|
|
||||||
<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="hand" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Profile -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="circle-user" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
@@ -55,31 +12,45 @@
|
|||||||
>
|
>
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
<!-- Context Menu -->
|
|
||||||
<a
|
|
||||||
href=""
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -right-2 -top-1"
|
|
||||||
><fa icon="ellipsis-vertical" class="fa-fw"></fa
|
|
||||||
></a>
|
|
||||||
|
|
||||||
View Plan
|
View Plan
|
||||||
</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">
|
||||||
<div>
|
<div>
|
||||||
|
<div class="block pb-4 flex gap-4">
|
||||||
|
<div class="flex-none w-16 pt-1">
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="projectId"
|
||||||
|
:iconSize="64"
|
||||||
|
class="block border border-slate-300 rounded-md"
|
||||||
|
></EntityIcon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-hidden">
|
||||||
<h2 class="text-xl font-semibold">{{ name }}</h2>
|
<h2 class="text-xl font-semibold">{{ name }}</h2>
|
||||||
<div class="flex justify-between gap-4 text-sm mb-3">
|
<div class="text-sm mb-3">
|
||||||
<span><fa icon="user" class="fa-fw text-slate-400"></fa> Rotary</span>
|
<div class="truncate">
|
||||||
<span
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
><fa icon="calendar" class="fa-fw text-slate-400"></fa
|
{{ issuer }}
|
||||||
>{{ timeSince }}
|
</div>
|
||||||
</span>
|
<div>
|
||||||
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ timeSince }}
|
||||||
|
</div>
|
||||||
|
<div v-if="latitude || longitude">
|
||||||
|
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
|
||||||
|
<a
|
||||||
|
:href="getOpenStreetMapUrl()"
|
||||||
|
target="_blank"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
Map View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-slate-500">
|
<div class="text-sm text-slate-500">
|
||||||
@@ -100,6 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
v-if="issuer == activeDid"
|
||||||
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="onEditClick()"
|
@click="onEditClick()"
|
||||||
@@ -108,67 +80,221 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Commit -->
|
<div>
|
||||||
<router-link
|
<div v-if="activeDid" class="text-center">
|
||||||
:to="{ name: 'new-edit-commitment' }"
|
<button
|
||||||
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
|
@click="openDialog({ name: 'you', did: activeDid })"
|
||||||
>Make Commitment</router-link
|
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
|
I gave…
|
||||||
|
</button>
|
||||||
|
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
|
||||||
|
</div>
|
||||||
|
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
|
||||||
|
|
||||||
<!-- Commitments -->
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
|
<li @click="openDialog()">
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="null"
|
||||||
|
:iconSize="64"
|
||||||
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
|
></EntityIcon>
|
||||||
|
<h3
|
||||||
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
Anonymous
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="contact in allContacts"
|
||||||
|
:key="contact.did"
|
||||||
|
@click="openDialog(contact)"
|
||||||
|
>
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="64"
|
||||||
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
|
></EntityIcon>
|
||||||
|
<h3
|
||||||
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
{{ contact.name || "(no name)" }}
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||||
|
<router-link
|
||||||
|
v-if="allContacts.length > 7"
|
||||||
|
:to="{ name: 'contact-gives' }"
|
||||||
|
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Show More Contacts…
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gifts to & from this -->
|
||||||
|
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Commitments</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
|
Given to this Project
|
||||||
|
</h3>
|
||||||
|
|
||||||
<ul class="text-sm border-t border-slate-300">
|
<ul class="text-sm border-t border-slate-300">
|
||||||
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
|
<li
|
||||||
<span>[Username]</span>
|
v-for="give in givesToThis"
|
||||||
|
:key="give.id"
|
||||||
|
class="py-1.5 border-b border-slate-300"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
<span
|
<span
|
||||||
>5 hours <fa icon="spinner" class="fa-fw text-slate-400"></fa
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
></span>
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
</li>
|
</span>
|
||||||
|
<span v-if="give.amount"
|
||||||
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
<span>[Username]</span>
|
{{ give.amount }}
|
||||||
<span
|
</span>
|
||||||
>US$ 20.00 <fa icon="circle-check" class="fa-fw text-lime-500"></fa
|
</div>
|
||||||
></span>
|
<div v-if="give.description" class="text-slate-500">
|
||||||
</li>
|
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.description }}
|
||||||
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
|
</div>
|
||||||
<span>[Username]</span>
|
|
||||||
<span
|
|
||||||
>0.1 BTC <fa icon="spinner" class="fa-fw text-slate-400"></fa
|
|
||||||
></span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
|
…and from this Project
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ul class="text-sm border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
v-for="give in givesByThis"
|
||||||
|
:key="give.id"
|
||||||
|
class="py-1.5 border-b border-slate-300"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span
|
||||||
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="give.amount"
|
||||||
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.amount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="give.description" class="text-slate-500">
|
||||||
|
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.description }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GiftedDialog
|
||||||
|
ref="customDialog"
|
||||||
|
@dialog-result="handleDialogResult"
|
||||||
|
message="Received from"
|
||||||
|
>
|
||||||
|
</GiftedDialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
import * as R from "ramda";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
|
didInfo,
|
||||||
|
GiverInputInfo,
|
||||||
|
GiverOutputInfo,
|
||||||
|
GiveServerRecord,
|
||||||
|
ResultWithType,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
|
||||||
@Options({
|
interface Notification {
|
||||||
components: {},
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ProjectViewView extends Vue {
|
export default class ProjectViewView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
activeDid = "";
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
expanded = false;
|
|
||||||
name = "";
|
|
||||||
description = "";
|
description = "";
|
||||||
|
expanded = false;
|
||||||
|
givesToThis: Array<GiveServerRecord> = [];
|
||||||
|
givesByThis: Array<GiveServerRecord> = [];
|
||||||
|
latitude = 0;
|
||||||
|
longitude = 0;
|
||||||
|
name = "";
|
||||||
|
issuer = "";
|
||||||
|
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
||||||
|
timeSince = "";
|
||||||
truncatedDesc = "";
|
truncatedDesc = "";
|
||||||
truncateLength = 40;
|
truncateLength = 40;
|
||||||
timeSince = "";
|
|
||||||
projectId = localStorage.getItem("projectId") || "";
|
async created() {
|
||||||
errorMessage = "";
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = accountsDB.accounts;
|
||||||
|
const accountsArr = await accounts?.toArray();
|
||||||
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||||
|
const account = accountsArr?.find((acc) => acc.did === this.activeDid);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
this.LoadProject(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load project records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity: IIdentifier) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
onEditClick() {
|
onEditClick() {
|
||||||
localStorage.setItem("projectId", this.projectId as string);
|
localStorage.setItem("projectId", this.projectId as string);
|
||||||
@@ -178,6 +304,16 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Isn't there a better way to make this available to the template?
|
||||||
|
didInfo(
|
||||||
|
did: string,
|
||||||
|
activeDid: string,
|
||||||
|
dids: Array<string>,
|
||||||
|
contacts: Array<Contact>,
|
||||||
|
) {
|
||||||
|
return didInfo(did, activeDid, dids, contacts);
|
||||||
|
}
|
||||||
|
|
||||||
expandText() {
|
expandText() {
|
||||||
this.expanded = true;
|
this.expanded = true;
|
||||||
}
|
}
|
||||||
@@ -191,11 +327,13 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/claim/byHandle/" +
|
"/api/claim/byHandle/" +
|
||||||
encodeURIComponent(this.projectId);
|
encodeURIComponent(this.projectId);
|
||||||
const token = await accessToken(identity);
|
const headers: RawAxiosRequestHeaders = {
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
};
|
||||||
|
if (identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
headers["Authorization"] = "Bearer " + token;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
@@ -206,42 +344,231 @@ export default class ProjectViewView extends Vue {
|
|||||||
const now = moment.now();
|
const now = moment.now();
|
||||||
this.timeSince = moment.utc(now).to(eventDate);
|
this.timeSince = moment.utc(now).to(eventDate);
|
||||||
}
|
}
|
||||||
|
this.issuer = resp.data.issuer;
|
||||||
this.name = resp.data.claim?.name || "(no name)";
|
this.name = resp.data.claim?.name || "(no name)";
|
||||||
this.description = resp.data.claim?.description || "(no description)";
|
this.description = resp.data.claim?.description || "(no description)";
|
||||||
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
||||||
|
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
||||||
|
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
||||||
} else if (resp.status === 404) {
|
} else if (resp.status === 404) {
|
||||||
// actually, axios throws an error so we never get here
|
// actually, axios throws an error so we never get here
|
||||||
this.errorMessage = "That project does not exist.";
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "That project does not exist.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError.response?.status === 404) {
|
if (serverError.response?.status === 404) {
|
||||||
this.errorMessage = "That project does not exist.";
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "That project does not exist.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.errorMessage =
|
this.$notify(
|
||||||
"Something went wrong retrieving that project." +
|
{
|
||||||
" See logs for more info.";
|
group: "alert",
|
||||||
console.error("Error retrieving project:", error);
|
type: "danger",
|
||||||
}
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving that project. See logs for more info.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error("Error retrieving project:", serverError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
const givesInUrl =
|
||||||
async created() {
|
this.apiServer +
|
||||||
await db.open();
|
"/api/v2/report/givesForPlans?planIds=" +
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
encodeURIComponent(JSON.stringify([this.projectId]));
|
||||||
const activeDid = settings?.activeDid || "";
|
try {
|
||||||
this.apiServer = settings?.apiServer || "";
|
const resp = await this.axios.get(givesInUrl, { headers });
|
||||||
|
if (resp.status === 200 && resp.data.data) {
|
||||||
await accountsDB.open();
|
this.givesToThis = resp.data.data;
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
|
||||||
if (num_accounts === 0) {
|
|
||||||
console.error("Problem! Should have a profile!");
|
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
this.$notify(
|
||||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
{
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
group: "alert",
|
||||||
this.LoadProject(identity);
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to retrieve gives to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving gives to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Error retrieving gives to this project:",
|
||||||
|
serverError.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const givesOutUrl =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/givesProvidedBy?providerId=" +
|
||||||
|
encodeURIComponent(this.projectId);
|
||||||
|
try {
|
||||||
|
const resp = await this.axios.get(givesOutUrl, { headers });
|
||||||
|
if (resp.status === 200 && resp.data.data) {
|
||||||
|
this.givesByThis = resp.data.data;
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to retrieve gives by this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving gives by project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Error retrieving gives by this project:",
|
||||||
|
serverError.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openDialog(contact: GiverInputInfo) {
|
||||||
|
const dialog: GiftedDialog = this.$refs.customDialog as GiftedDialog;
|
||||||
|
dialog.open(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenStreetMapUrl() {
|
||||||
|
// Google URL is https://maps.google.com/?q=LAT,LONG
|
||||||
|
return (
|
||||||
|
"https://www.openstreetmap.org/?mlat=" +
|
||||||
|
this.latitude +
|
||||||
|
"&mlon=" +
|
||||||
|
this.longitude +
|
||||||
|
"#map=15/" +
|
||||||
|
this.latitude +
|
||||||
|
"/" +
|
||||||
|
this.longitude
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDialogResult(result: GiverOutputInfo) {
|
||||||
|
if (result.action === "confirm") {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.recordGive(
|
||||||
|
result.giver?.did,
|
||||||
|
result.description,
|
||||||
|
result.hours,
|
||||||
|
).then(() => {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// action was not "confirm" so do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param giverDid may be null
|
||||||
|
* @param description may be an empty string
|
||||||
|
* @param hours may be 0
|
||||||
|
*/
|
||||||
|
async recordGive(giverDid?: string, description?: string, hours?: number) {
|
||||||
|
if (!this.activeDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identity before you can record a give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description && !hours) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must enter a description or some number of hours.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
const result = await createAndSubmitGive(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
identity,
|
||||||
|
giverDid,
|
||||||
|
this.activeDid,
|
||||||
|
description,
|
||||||
|
hours,
|
||||||
|
this.projectId,
|
||||||
|
);
|
||||||
|
if (result.type == "success") {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: "That gift was recorded.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log("Error with give creation:", result);
|
||||||
|
if (result.type != "error") {
|
||||||
|
console.log(
|
||||||
|
"... and it has an unexpected result type of",
|
||||||
|
(result as ResultWithType).type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const message =
|
||||||
|
result?.error?.userMessage ||
|
||||||
|
"There was an error recording the Give.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Projects"></QuickNav>
|
||||||
<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">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
@@ -49,7 +7,8 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Quick Search -->
|
<!-- Quick Search -->
|
||||||
<form id="QuickSearch" class="mb-4 flex">
|
|
||||||
|
<div id="QuickSearch" class="mb-4 flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search…"
|
placeholder="Search…"
|
||||||
@@ -60,7 +19,7 @@
|
|||||||
>
|
>
|
||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
<!-- New Project -->
|
<!-- New Project -->
|
||||||
<button
|
<button
|
||||||
@@ -70,8 +29,17 @@
|
|||||||
<fa icon="plus" class="fa-fw"></fa>
|
<fa icon="plus" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Loading Animation -->
|
||||||
|
<div
|
||||||
|
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||||
|
v-if="isLoading"
|
||||||
|
>
|
||||||
|
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul class="">
|
<InfiniteScroll @reached-bottom="loadMoreData">
|
||||||
|
<ul class="border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300"
|
class="border-b border-slate-300"
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
@@ -82,10 +50,11 @@
|
|||||||
class="block py-4 flex gap-4"
|
class="block py-4 flex gap-4"
|
||||||
>
|
>
|
||||||
<div class="flex-none w-12">
|
<div class="flex-none w-12">
|
||||||
<img
|
<EntityIcon
|
||||||
src="https://picsum.photos/200/200?random=1"
|
:entityId="project.handleId"
|
||||||
class="w-full rounded"
|
:iconSize="48"
|
||||||
/>
|
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||||
|
></EntityIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
@@ -97,25 +66,113 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</InfiniteScroll>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as R from "ramda";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { accountsDB, db } from "@/db/index";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import { ProjectData } from "@/libs/endorserServer";
|
||||||
|
|
||||||
@Options({
|
interface Notification {
|
||||||
components: {},
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { InfiniteScroll, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ProjectsView extends Vue {
|
export default class ProjectsView extends Vue {
|
||||||
apiServer = "";
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
projects: { handleId: string; name: string; description: string }[] = [];
|
|
||||||
|
|
||||||
|
apiServer = "";
|
||||||
|
projects: ProjectData[] = [];
|
||||||
|
current: IIdentifier;
|
||||||
|
isLoading = false;
|
||||||
|
alertTitle = "";
|
||||||
|
alertMessage = "";
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core project data loader
|
||||||
|
* @param url the url used to fetch the data
|
||||||
|
* @param token Authorization token
|
||||||
|
**/
|
||||||
|
async dataLoader(url: string, token: string) {
|
||||||
|
const headers: { [key: string]: string } = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
const resp = await this.axios.get(url, { headers });
|
||||||
|
if (resp.status === 200 || !resp.data.data) {
|
||||||
|
const plans: ProjectData[] = resp.data.data;
|
||||||
|
for (const plan of plans) {
|
||||||
|
const { name, description, handleId, rowid } = plan;
|
||||||
|
this.projects.push({ name, description, handleId, rowid });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Bad server response & data:", resp.status, resp.data);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to get projects from the server. Try again later.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Got error loading projects:", error.message || error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Got an error loading projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data loader used by infinite scroller
|
||||||
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
|
**/
|
||||||
|
async loadMoreData(payload: boolean) {
|
||||||
|
if (this.projects.length > 0 && payload) {
|
||||||
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
|
const url = `${this.apiServer}/api/v2/report/plansByIssuer?beforeId=${latestProject.rowid}`;
|
||||||
|
const token = await accessToken(this.current);
|
||||||
|
await this.dataLoader(url, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle clicking on a project entry found in the list
|
||||||
|
* @param id of the project
|
||||||
|
**/
|
||||||
onClickLoadProject(id: string) {
|
onClickLoadProject(id: string) {
|
||||||
localStorage.setItem("projectId", id);
|
localStorage.setItem("projectId", id);
|
||||||
const route = {
|
const route = {
|
||||||
@@ -124,54 +181,75 @@ export default class ProjectsView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load projects initially
|
||||||
|
* @param identity of the user
|
||||||
|
**/
|
||||||
async LoadProjects(identity: IIdentifier) {
|
async LoadProjects(identity: IIdentifier) {
|
||||||
const url = this.apiServer + "/api/v2/report/plansByIssuer";
|
const url = `${this.apiServer}/api/v2/report/plansByIssuer`;
|
||||||
const token = await accessToken(identity);
|
const token: string = await accessToken(identity);
|
||||||
const headers = {
|
await this.dataLoader(url, token);
|
||||||
"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.error("Got error loading projects: ", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
public async getIdentity(activeDid: string) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load project records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 'created' hook runs when the Vue instance is first created
|
||||||
|
**/
|
||||||
async created() {
|
async created() {
|
||||||
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
const activeDid = settings?.activeDid || "";
|
const activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
|
||||||
await accountsDB.open();
|
if (this.numAccounts === 0) {
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
console.error("No accounts found.");
|
||||||
if (num_accounts === 0) {
|
this.$notify(
|
||||||
console.error("Problem! Should have a profile!");
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You need an identity to load your projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const identity = await this.getIdentity(activeDid);
|
||||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
this.current = identity;
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
|
||||||
this.LoadProjects(identity);
|
this.LoadProjects(identity);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error initializing:", err);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong loading your projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handling clicking on the new project button
|
||||||
|
**/
|
||||||
onClickNewProject(): void {
|
onClickNewProject(): void {
|
||||||
localStorage.removeItem("projectId");
|
localStorage.removeItem("projectId");
|
||||||
const route = {
|
const route = {
|
||||||
|
|||||||
@@ -1,52 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
|
||||||
<ul class="flex text-2xl p-2 gap-2">
|
|
||||||
<!-- Home Feed -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
|
|
||||||
<fa icon="house-chimney" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Search -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'discover' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Projects -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'projects' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="folder-open" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Contacts -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contacts' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="users" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Profile -->
|
|
||||||
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="circle-user" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
@@ -67,48 +20,63 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeAccount">
|
<div v-if="activeAccount">
|
||||||
<p>
|
<p class="text-center mb-4">
|
||||||
BEWARE: Anyone who gets hold of this mnemonic seed phrase will be able
|
<b class="text-red-600">BEWARE!</b> Anyone who has this seed phrase will
|
||||||
impersonate you and take over any digital holdings based on it. So only
|
be able impersonate you and take over any digital holdings based on it.
|
||||||
reveal it when you are in a private place out of sight of other eyes,
|
Reveal it when you are somewhere only you can see your screen, and
|
||||||
and only record it in something private -- don't take a screenshot or
|
record it somewhere only you have access.
|
||||||
send it to any online service.
|
<i>Don't take a screenshot or send it to any online service.</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p v-if="numAccounts > 1">
|
||||||
|
<b class="text-orange-600">Note:</b> You have more than one identity
|
||||||
|
stored in this browser. If they are all based on the same seed as the
|
||||||
|
current identity, this one backup is sufficient; however, if you have
|
||||||
|
different seeds for other identities, you will have to back them up
|
||||||
|
separately.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-slate-100 rounded-md overflow-hidden p-4 mb-4">
|
||||||
<button
|
<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="showSeedPhrase"
|
@click="showSeedPhrase"
|
||||||
>
|
>
|
||||||
Click here when you're ready to see it.
|
Reveal my Seed Phrase
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p v-if="showSeed">{{ activeAccount.mnemonic }}</p>
|
<p v-if="showSeed" class="text-center text-slate-700 mt-2">
|
||||||
|
{{ activeAccount.mnemonic }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>You do not have an active identity.</div>
|
<div v-else>You do not have an active identity.</div>
|
||||||
|
|
||||||
<!-- This same popup code is in many files. -->
|
|
||||||
<div v-bind:class="computedAlertClassNames()">
|
|
||||||
<button
|
|
||||||
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
|
||||||
@click="onClickClose()"
|
|
||||||
>
|
|
||||||
<fa icon="xmark"></fa>
|
|
||||||
</button>
|
|
||||||
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
|
||||||
<p>{{ alertMessage }}</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
|
||||||
@Component
|
interface Account {
|
||||||
|
mnemonic: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({ components: { QuickNav } })
|
||||||
export default class SeedBackupView extends Vue {
|
export default class SeedBackupView extends Vue {
|
||||||
activeAccount = null;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
activeAccount: Account | null | undefined = null;
|
||||||
|
numAccounts = 0;
|
||||||
showSeed = false;
|
showSeed = false;
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
// 'created' hook runs when the Vue instance is first created
|
||||||
@@ -120,43 +88,24 @@ export default class SeedBackupView extends Vue {
|
|||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
this.numAccounts = accounts.length;
|
||||||
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
console.error("Got an error loading an identity:", err);
|
console.error("Got an error loading an identity:", err);
|
||||||
this.alertTitle = "Error Loading Account";
|
this.$notify(
|
||||||
this.alertMessage = "Got an error loading your seed data.";
|
{
|
||||||
this.isAlertVisible = true;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Loading Account",
|
||||||
|
text: "Got an error loading your seed data.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showSeedPhrase() {
|
showSeedPhrase() {
|
||||||
this.showSeed = true;
|
this.showSeed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
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>
|
||||||
|
|||||||
@@ -10,36 +10,56 @@
|
|||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<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 have an identity to import?
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
@click="onClickYes()"
|
@click="onClickYes()"
|
||||||
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 uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@click="onClickNo()"
|
@click="onClickNo()"
|
||||||
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 mt-2"
|
||||||
>Yes</a
|
|
||||||
>
|
>
|
||||||
|
Yes
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="numAccounts > 0"
|
||||||
|
@click="onClickDerive()"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
||||||
|
>
|
||||||
|
Derive New Address from Seed Imported Previously
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { accountsDB } from "@/db/index";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class StartView extends Vue {
|
export default class StartView extends Vue {
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
public onClickYes() {
|
public onClickYes() {
|
||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "new-identifier" });
|
||||||
}
|
}
|
||||||
|
|
||||||
public onClickNo() {
|
public onClickNo() {
|
||||||
this.$router.push({ name: "import-account" });
|
this.$router.push({ name: "import-account" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onClickDerive() {
|
||||||
|
this.$router.push({ name: "import-derive" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,52 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
|
||||||
<ul class="flex text-2xl p-2 gap-2">
|
|
||||||
<!-- Home Feed -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
|
|
||||||
<fa icon="house-chimney" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Search -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'discover' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Projects -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'projects' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="folder-open" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Contacts -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contacts' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="users" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Profile -->
|
|
||||||
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
>
|
|
||||||
<fa icon="circle-user" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
@@ -61,7 +14,7 @@
|
|||||||
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
|
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
|
||||||
<li>Each will show at their time of appearance relative to all others.</li>
|
<li>Each will show at their time of appearance relative to all others.</li>
|
||||||
<li>Note that the ones on the left and right edges are randomized
|
<li>Note that the ones on the left and right edges are randomized
|
||||||
because not all their positional data is visible to you.
|
because their data isn't all visible to you.
|
||||||
</li>
|
</li>
|
||||||
<!-- eslint-enable -->
|
<!-- eslint-enable -->
|
||||||
</ul>
|
</ul>
|
||||||
@@ -75,110 +28,63 @@
|
|||||||
{{ worldProperties.endTime }}
|
{{ worldProperties.endTime }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="worldProperties.animationDurationSeconds">
|
<div v-if="worldProperties.animationDurationSeconds">
|
||||||
<label>Animation duration: </label>
|
<label>Animation Time: </label>
|
||||||
{{ worldProperties.animationDurationSeconds }} seconds
|
{{ worldProperties.animationDurationSeconds }} seconds
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="float-right" @click="captureGraphics()">Screenshot</button>
|
<button class="float-right" @click="captureGraphics()">Screenshot</button>
|
||||||
|
|
||||||
<!-- Another place to play with the sizing is in Resizer.setSize -->
|
|
||||||
<div id="scene-container" class="h-screen"></div>
|
<div id="scene-container" class="h-screen"></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 { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
|
import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { World } from "@/components/World/World.js";
|
import { World } from "@/components/World/World.js";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
|
||||||
interface WorldProperties {
|
interface RendererSVGType {
|
||||||
startTime?: string;
|
domElement: Element;
|
||||||
endTime?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component
|
interface Dictionary<T> {
|
||||||
|
[key: string]: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({ components: { World, QuickNav } })
|
||||||
export default class StatisticsView extends Vue {
|
export default class StatisticsView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
world: World;
|
world: World;
|
||||||
worldProperties: WorldProperties = {};
|
worldProperties: Dictionary<number> = {};
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
try {
|
||||||
const container = document.querySelector("#scene-container");
|
const container = document.querySelector("#scene-container");
|
||||||
const newWorld = new World(container, this);
|
const newWorld = new World(container, this);
|
||||||
newWorld.start();
|
newWorld.start();
|
||||||
this.world = newWorld;
|
this.world = newWorld;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as Error;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Mounting Error",
|
||||||
|
text: error.message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public captureGraphics() {
|
public captureGraphics() {
|
||||||
/**
|
|
||||||
// from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#examples
|
|
||||||
// Adds a blank image
|
|
||||||
const dataBlob = document
|
|
||||||
.querySelector("#scene-container")
|
|
||||||
.firstChild.toBlob((blob) => {
|
|
||||||
const newImg = document.createElement("img");
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
newImg.onload = () => {
|
|
||||||
// no longer need to read the blob so it's revoked
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
newImg.src = url;
|
|
||||||
document.body.appendChild(newImg);
|
|
||||||
});
|
|
||||||
**/
|
|
||||||
|
|
||||||
/**
|
|
||||||
// Yields a blank page with the iframe below
|
|
||||||
const dataUrl = document
|
|
||||||
.querySelector("#scene-container")
|
|
||||||
.firstChild.toDataURL("image/png");
|
|
||||||
**/
|
|
||||||
|
|
||||||
/**
|
|
||||||
// Yields a blank page with the iframe below
|
|
||||||
const dataUrl = this.world.renderer.domElement.toDataURL("image/png");
|
|
||||||
**/
|
|
||||||
|
|
||||||
/**
|
|
||||||
// Show the image in a new tab
|
|
||||||
const iframe = `
|
|
||||||
<iframe
|
|
||||||
src="${dataUrl}"
|
|
||||||
frameborder="0"
|
|
||||||
style="border:0; top:0px; left:0px; bottom:0px; right:0px; width:100%; height:100%;"
|
|
||||||
allowfullscreen>
|
|
||||||
</iframe>`;
|
|
||||||
const win = window.open();
|
|
||||||
win.document.open();
|
|
||||||
win.document.write(iframe);
|
|
||||||
win.document.close();
|
|
||||||
**/
|
|
||||||
|
|
||||||
// from https://stackoverflow.com/a/17407392/845494
|
|
||||||
// This yields a file with funny formatting.
|
|
||||||
//const image = const dataUrl.replace("image/png", "image/octet-stream");
|
|
||||||
|
|
||||||
/**
|
|
||||||
// Yields a blank image at the bottom of the page
|
|
||||||
// from https://discourse.threejs.org/t/save-screenshot-on-server/39900/3
|
|
||||||
const img = new Image();
|
|
||||||
img.src = this.world.renderer.domElement.toDataURL();
|
|
||||||
document.body.appendChild(img);
|
|
||||||
**/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This yields an SVG that only shows white and black highlights
|
* This yields an SVG that only shows white and black highlights
|
||||||
// from https://stackoverflow.com/questions/27632621/exporting-from-three-js-scene-to-svg-or-other-vector-format
|
// from https://stackoverflow.com/questions/27632621/exporting-from-three-js-scene-to-svg-or-other-vector-format
|
||||||
@@ -186,49 +92,15 @@ export default class StatisticsView extends Vue {
|
|||||||
const rendererSVG = new SVGRenderer();
|
const rendererSVG = new SVGRenderer();
|
||||||
rendererSVG.setSize(window.innerWidth, window.innerHeight);
|
rendererSVG.setSize(window.innerWidth, window.innerHeight);
|
||||||
rendererSVG.render(this.world.scene, this.world.camera);
|
rendererSVG.render(this.world.scene, this.world.camera);
|
||||||
//document.body.appendChild(rendererSVG.domElement);
|
|
||||||
ExportToSVG(rendererSVG, "test.svg");
|
ExportToSVG(rendererSVG, "test.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
public setWorldProperty(propertyName, propertyValue) {
|
public setWorldProperty(propertyName: string, propertyValue: number) {
|
||||||
this.worldProperties[propertyName] = propertyValue;
|
this.worldProperties[propertyName] = propertyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
alertTitle = "";
|
|
||||||
alertMessage = "";
|
|
||||||
isAlertVisible = false;
|
|
||||||
|
|
||||||
public setAlert(title, message) {
|
|
||||||
this.alertTitle = title;
|
|
||||||
this.alertMessage = message;
|
|
||||||
this.isAlertVisible = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onClickClose() {
|
function ExportToSVG(rendererSVG: RendererSVGType, filename: string) {
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExportToSVG(rendererSVG, filename) {
|
|
||||||
const XMLS = new XMLSerializer();
|
const XMLS = new XMLSerializer();
|
||||||
const svgfile = XMLS.serializeToString(rendererSVG.domElement);
|
const svgfile = XMLS.serializeToString(rendererSVG.domElement);
|
||||||
const svgData = svgfile;
|
const svgData = svgfile;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
@@ -12,14 +15,17 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": "./src",
|
||||||
"types": [
|
"types": [
|
||||||
"webpack-env"
|
"webpack-env"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/components/*": ["components/*"],
|
||||||
"src/*"
|
"@/views/*": ["views/*"],
|
||||||
]
|
"@/db/*": ["db/*"],
|
||||||
|
"@/libs/*": ["libs/*"],
|
||||||
|
"@/constants/*": ["constants/*"],
|
||||||
|
"@/store/*": ["store/*"],
|
||||||
},
|
},
|
||||||
"lib": [
|
"lib": [
|
||||||
"esnext",
|
"esnext",
|
||||||
|
|||||||
389
web-push.md
Normal file
389
web-push.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
Web Push notifications is a web browser messaging protocol defined by the W3C.
|
||||||
|
|
||||||
|
Discussions of this interesting technology are clouded because of a
|
||||||
|
terminological morass.
|
||||||
|
|
||||||
|
To understand how Web Push operates, we need to observe that are three (and
|
||||||
|
potentially four) parties involved. These are:
|
||||||
|
|
||||||
|
1) The user's web browser. Let's call that BROWSER
|
||||||
|
2) The Web Push Service Provider which is operated by the organization
|
||||||
|
controlling the web browser's source code. Here named PROVIDER. An example of a
|
||||||
|
PROVIDER is FCM (Firebase Cloud Messaging) which is owned by Google.
|
||||||
|
3) The Web Application that a user is visiting from their web browser. Let's
|
||||||
|
call this the SERVICE (short for Web Push application service)
|
||||||
|
4) A Custom Web Push Intermediary Service, either third party or self-hosted.
|
||||||
|
Called INTERMEDIARY here. FCM also may fit in this category if the SERVICE
|
||||||
|
has an API key from FCM.]
|
||||||
|
|
||||||
|
The workflow works like this:
|
||||||
|
|
||||||
|
BROWSER visits a website which hosts a SERVICE.
|
||||||
|
|
||||||
|
The SERVICE asks BROWSER for its permission to subscribe to messages coming
|
||||||
|
from the SERVICE.
|
||||||
|
|
||||||
|
The SERVICE will provide context and obtain explicit permission before prompting
|
||||||
|
for notification permission:
|
||||||
|
|
||||||
|
In order to provide this context and explict permission a two-step opt-in process
|
||||||
|
where the user is first presented with a pre-permission dialog box that explains
|
||||||
|
what the notifications are for and why they are useful. This may help reduce the
|
||||||
|
possibility of users clicking "don't allow".
|
||||||
|
|
||||||
|
Now, to explain what happens in Typescript, we can activate a browser's
|
||||||
|
permission dialogue in this manner:
|
||||||
|
|
||||||
|
```
|
||||||
|
function askPermission(): Promise<NotificationPermission> {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const permissionResult = Notification.requestPermission(function(result) {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permissionResult) {
|
||||||
|
permissionResult.then(resolve, reject);
|
||||||
|
}
|
||||||
|
}).then(function(permissionResult) {
|
||||||
|
if (permissionResult !== 'granted') {
|
||||||
|
throw new Error("We weren't granted permission.");
|
||||||
|
}
|
||||||
|
return permissionResult;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The Notification.permission property indicates the permission level for the
|
||||||
|
current session and returns one of the following string values:
|
||||||
|
|
||||||
|
'granted': The user has granted permission for notifications.
|
||||||
|
'denied': The user has denied permission for notifications.
|
||||||
|
'default': The user has not made a choice yet.
|
||||||
|
|
||||||
|
Once the user has granted permission, the client application registers a service
|
||||||
|
worker using the `ServiceWorkerRegistration` API.
|
||||||
|
|
||||||
|
The `ServiceWorkerRegistration` API is accessible via the browser's `navigator`
|
||||||
|
object and the `navigator.serviceWorker` child object and ultimately directly
|
||||||
|
accessible via the navigator.serviceWorker.register method which also creates
|
||||||
|
the service worker or the navigator.serviceWorker.getRegistration method.
|
||||||
|
|
||||||
|
Once you have a `ServiceWorkerRegistration` object, that object will provide a
|
||||||
|
child object named `pushManager` through which subscription and management of
|
||||||
|
subscriptions may be done.
|
||||||
|
|
||||||
|
Let's go through the `register` method first:
|
||||||
|
|
||||||
|
```
|
||||||
|
navigator.serviceWorker.register('sw.js', { scope: '/' })
|
||||||
|
.then(function(registration) {
|
||||||
|
console.log('Service worker registered successfully:', registration);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.log('Service worker registration failed:', error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `sw.js` file contains the logic for what a service worker should do.
|
||||||
|
It executes in a separate thread of execution from the web page but provides a
|
||||||
|
means of communicating between itself and the web page via messages.
|
||||||
|
|
||||||
|
Note that there is a scope can specify what network requests it may
|
||||||
|
intercept.
|
||||||
|
|
||||||
|
The Vue project already has its own service worker but it is possible to
|
||||||
|
create multiple service worker files by registering them on different scopes.
|
||||||
|
|
||||||
|
It is useful architecturally to specify a separate server worker file.
|
||||||
|
|
||||||
|
In the case of web push, the path of the scope only has reference to the domain
|
||||||
|
of the service worker and no relationship to the pathing for the web push
|
||||||
|
server. In order to specify more than one server workers each needs to be on
|
||||||
|
different scope paths!
|
||||||
|
|
||||||
|
Here's a version which can be used for testing locally. Note there can be
|
||||||
|
caching issues in your browser! Incognito is highly recommended.
|
||||||
|
|
||||||
|
sw-dev.ts
|
||||||
|
```
|
||||||
|
self.addEventListener('push', function(event: PushEvent) {
|
||||||
|
console.log('Received a push message', event);
|
||||||
|
|
||||||
|
const title = 'Push message';
|
||||||
|
const body = 'The message body';
|
||||||
|
const icon = '/images/icon-192x192.png';
|
||||||
|
const tag = 'simple-push-demo-notification-tag';
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: icon,
|
||||||
|
tag: tag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
vue.config.js
|
||||||
|
```
|
||||||
|
module.exports = {
|
||||||
|
pwa: {
|
||||||
|
workboxOptions: {
|
||||||
|
importScripts: ['sw-dev.ts']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once we have the service worker registered and the ServiceWorkerRegistration is
|
||||||
|
returned, we then have access to a `pushManager` property object. This property
|
||||||
|
allows us to continue with the web push work flow.
|
||||||
|
|
||||||
|
In the next step, BROWSER requests a data structure from SERVICE called a VAPID
|
||||||
|
(Voluntary Application Server Identification) which is the public key from a
|
||||||
|
key-pair.
|
||||||
|
|
||||||
|
The VAPID is a specification used to identify the application server (i.e. the
|
||||||
|
SERVICE server) that is sending push messages through a push PROVIDER. It's an
|
||||||
|
authentication mechanism that allows the server to demonstrate its identity to
|
||||||
|
the push PROVIDER, by use of a public and private key pair. These keys are used
|
||||||
|
by the SERVICE in encrypting messages being sent to the BROWSER, as well as
|
||||||
|
being used by the BROWSER in decrypting the messages coming from the SERVICE.
|
||||||
|
|
||||||
|
The VAPID (Voluntary Application Server Identification) key provides more
|
||||||
|
security and authenticity for web push notifications in the following ways:
|
||||||
|
|
||||||
|
Identifying the Application Server:
|
||||||
|
|
||||||
|
The VAPID key is used to identify the application server that is sending
|
||||||
|
the push notifications. This ensures that the push notifications are
|
||||||
|
authentic and not sent by a malicious third party.
|
||||||
|
|
||||||
|
Encrypting the Messages:
|
||||||
|
|
||||||
|
The VAPID key is used to sign the push notifications sent by the
|
||||||
|
application server, ensuring that they are not tampered with during
|
||||||
|
transmission. This provides an additional layer of security and
|
||||||
|
authenticity for the push notifications.
|
||||||
|
|
||||||
|
Adding Contact Information:
|
||||||
|
|
||||||
|
The VAPID key allows a web application to add contact information to
|
||||||
|
the push messages sent to the browser push service. This enables the
|
||||||
|
push service to contact the application server in case of need or
|
||||||
|
provide additional debug information about the push messages.
|
||||||
|
|
||||||
|
Improving Delivery Rates:
|
||||||
|
|
||||||
|
Using the VAPID key can help improve the overall performance of web push
|
||||||
|
notifications, specifically improving delivery rates. By streamlining the
|
||||||
|
delivery process, the chance of delivery errors along the way is lessened.
|
||||||
|
|
||||||
|
If the BROWSER accepts and grants permission to subscribe to receiving from the
|
||||||
|
SERVICE Web Push messages, then the BROWSER makes a subscription request to
|
||||||
|
PROVIDER which creates and stores a special URL for that BROWSER.
|
||||||
|
|
||||||
|
Here's a bit of code describing the above process:
|
||||||
|
|
||||||
|
```
|
||||||
|
// b64 is the VAPID
|
||||||
|
b64 = 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U';
|
||||||
|
const applicationServerKey = urlBase64ToUint8Array(b64);
|
||||||
|
const options: PushSubscriptionOptions = {
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: applicationServerKey
|
||||||
|
};
|
||||||
|
|
||||||
|
registration.pushManager.subscribe(options)
|
||||||
|
.then(function(subscription) {
|
||||||
|
console.log('Push subscription successful:', subscription);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Push subscription failed:', error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `applicationServerKey` variable contains the VAPID public
|
||||||
|
key, which is converted to a `Uint8Array` using a function such as this:
|
||||||
|
|
||||||
|
```
|
||||||
|
export function toUint8Array(base64String: string, atobFn: typeof atob): Uint8Array {
|
||||||
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
|
||||||
|
const rawData = atobFn(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The options object is of type `PushSubscriptionOptions`, which includes the
|
||||||
|
`userVisibleOnly` and `applicationServerKey` (ie VAPID public key) properties.
|
||||||
|
|
||||||
|
options: An object that contains the options used for creating the
|
||||||
|
subscription. This object itself has the following sub-properties:
|
||||||
|
|
||||||
|
applicationServerKey: A public key your push service uses for application
|
||||||
|
server identification. This is normally a Uint8Array.
|
||||||
|
|
||||||
|
userVisibleOnly: A boolean value indicating that the push messages that
|
||||||
|
are sent should be made visible to the user through a notification.
|
||||||
|
This is often set to true.
|
||||||
|
|
||||||
|
The subscribe() method returns a `Promise` that resolves to a `PushSubscription`
|
||||||
|
object containing details of the subscription, such as the endpoint URL and the
|
||||||
|
public key. The returned data would have a form like this:
|
||||||
|
|
||||||
|
{
|
||||||
|
"endpoint": "https://some.pushservice.com/some/unique/identifier",
|
||||||
|
"expirationTime": null,
|
||||||
|
"keys": {
|
||||||
|
"p256dh": "some_base64_encoded_string",
|
||||||
|
"auth": "some_other_base64_encoded_string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint: A string representing the endpoint URL for the push service. This
|
||||||
|
URL is essentially the push service address to which the push message would
|
||||||
|
be sent for this particular subscription.
|
||||||
|
|
||||||
|
expirationTime: A DOMHighResTimeStamp (which is basically a number or null)
|
||||||
|
representing the subscription's expiration time in milliseconds since
|
||||||
|
01 January, 1970 UTC. This can be null if the subscription never expires.
|
||||||
|
|
||||||
|
The BROWSER will, internally, then use that URL to check for incoming messages
|
||||||
|
by way of the service worker we described earlier. The BROWSER also sends this
|
||||||
|
URL back to SERVICE which will use that URL to send messages to the BROWSER via
|
||||||
|
the PROVIDER.
|
||||||
|
|
||||||
|
Ultimately, the actual internal process of receiving messages varies from BROWSER
|
||||||
|
to BROWSER. Approaches vary from long-polling HTTP connections to WebSockets. A
|
||||||
|
lot of handwaving and voodoo magic. The bottom line is that the BROWSER itself
|
||||||
|
manages the connection to the PROVIDER whilst the SERVICE must send messages
|
||||||
|
via the PROVIDER so that they reach the BROWSER service worker.
|
||||||
|
|
||||||
|
Just to remind us that in our service worker our code for receiving messages
|
||||||
|
will look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
self.addEventListener('push', function(event: PushEvent) {
|
||||||
|
console.log('Received a push message', event);
|
||||||
|
|
||||||
|
const title = 'Push message';
|
||||||
|
const body = 'The message body';
|
||||||
|
const icon = '/images/icon-192x192.png';
|
||||||
|
const tag = 'simple-push-demo-notification-tag';
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: icon,
|
||||||
|
tag: tag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Now to address the issue of receiving notification messages on mobile devices.
|
||||||
|
It should be noted that Web Push messages are only received when BROWSER is
|
||||||
|
open, except in the cases of Chrome and Firefox mobile BROWSERS. In iOS, the
|
||||||
|
mobile application (in our case a PWA) must be added to the Home Screen and
|
||||||
|
permissions must be explicitly granted that allow the application to receive
|
||||||
|
push notifications. Further, with an iOS device the user must enable wake on
|
||||||
|
notification to have their device light-up when it receives a notification
|
||||||
|
(https://support.apple.com/enus/HT208081).
|
||||||
|
|
||||||
|
So what about #4? - The INTERMEDIARY. Well, It is possible under very special
|
||||||
|
circumstances to create your own Web Push PROVIDER. The only case I've found so
|
||||||
|
far relates to making an Android Custom ROM. (An Android Custom ROM is a
|
||||||
|
customized version of the Android Operating System.) There are open source
|
||||||
|
IMTERMEDIARY products such as UnifiedPush (https://unifiedpush.org/) which can
|
||||||
|
fulfill this role. If you are using iOS you are not permitted to make or use
|
||||||
|
your own custom Web Push PROVIDER. Apple will never allow anyone to do that.
|
||||||
|
Apple has none of its own.
|
||||||
|
|
||||||
|
It is, however, possible to have a sort of proxy working between your SERVICE
|
||||||
|
and FCM (or iOS). Services that mash up various Push notification services (like
|
||||||
|
OneSignal) can perform in the role of such proxies.
|
||||||
|
|
||||||
|
#4 -The INTERMEDIARY- doesn't appear to be anything we should be spending our
|
||||||
|
time on.
|
||||||
|
|
||||||
|
A BROWSER may also remove a subscription. In order to remove a subscription,
|
||||||
|
the registration record must be retrieved from the serviceWorker using
|
||||||
|
`navigator.serviceWorker.ready`. Within the `ready` property is the
|
||||||
|
`pushManager` which has a `getSubscription` method. Once you have the
|
||||||
|
subscription object, you may call the `unsubscribe` method. `unsubscribe` is
|
||||||
|
asynchronnous and returns a boolean true if it is successful in removing the
|
||||||
|
subscription and false if not.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
async function unsubscribeFromPush() {
|
||||||
|
// Check if the browser supports service workers
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
// Get the registration object for the service worker
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
|
||||||
|
// Get the existing subscription
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
// Unsubscribe
|
||||||
|
const successful = await subscription.unsubscribe();
|
||||||
|
if (successful) {
|
||||||
|
console.log("Successfully unsubscribed from push notifications.");
|
||||||
|
// You can also inform your server to remove this subscription
|
||||||
|
} else {
|
||||||
|
console.log("Failed to unsubscribe from push notifications.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No subscription was found.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Service workers are not supported by this browser.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from push notifications
|
||||||
|
unsubscribeFromPush().catch((err) => {
|
||||||
|
console.error("An error occurred while unsubscribing from push notifications", err);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: We could offer an option within the app to "mute" these notifications. This wouldn't turn off the notifications at the browser level, but you could make it so that your Service Worker doesn't display them even if it receives them.
|
||||||
|
|
||||||
|
|
||||||
|
## NOTIFICATION DIALOG WORKFLOW
|
||||||
|
|
||||||
|
# ON APP FIRST-LAUNCH:
|
||||||
|
The user is periodically presented with the notification permission dialog that asks them if they want to turn on notifications. User is given 3 choices:
|
||||||
|
|
||||||
|
- "Turn on Notifications": triggers the browser's own notification permission prompt.
|
||||||
|
- "Maybe Later": dismisses the dialog, to reappear at a later instance. (The next time the user launches the app? After X amount of days? A combination of both?)
|
||||||
|
- "Never": dismisses the dialog; app remembers to not automatically present the dialog again.
|
||||||
|
|
||||||
|
# IF THE USER CHOOSES "NEVER":
|
||||||
|
The dialog can still be accessed via the Notifications toggle switch in `AccountViewView` (which also tells the user if notifications are turned on or off).
|
||||||
|
|
||||||
|
# TO TEMPORARILY MUTE NOTIFICATIONS:
|
||||||
|
While notifications are turned on, the user can tap on the Mute Notifications toggle switch in `AccountViewView` (visible only when notifications are turned on) to trigger the Mute Notifications Dialog. User is given the following choices:
|
||||||
|
|
||||||
|
- Several "Mute for X Hour/s" buttons to temporarily mute notifications.
|
||||||
|
- "Mute until I turn it back on" button to indefinitely mute notifications.
|
||||||
|
- "Cancel" to make no changes and dismiss the dialog.
|
||||||
|
|
||||||
|
# TO UNMUTE NOTIFICATIONS:
|
||||||
|
Simply tap on the Mute Notifications toggle switch in `AccountViewView` to immediately unmute notifications. No dialog needed.
|
||||||
|
|
||||||
|
# TO TURN OFF NOTIFICATIONS:
|
||||||
|
While notifications are turned on, the user can tap on the App Notifications toggle switch in `AccountViewView` to trigger the Turn Off Notifications Dialog. User is given the following choices:
|
||||||
|
|
||||||
|
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
|
||||||
|
- "Leave it On" to make no changes and dismiss the dialog.
|
||||||
Reference in New Issue
Block a user