forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bdd0743a3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@ node_modules
|
|||||||
signature.bin
|
signature.bin
|
||||||
*.pem
|
*.pem
|
||||||
verified.txt
|
verified.txt
|
||||||
myenv
|
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
|
|||||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,23 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.3] - 2023.11
|
|
||||||
### Added
|
|
||||||
- Contact name editing
|
|
||||||
### Changed
|
|
||||||
- Don't show actions on front page if not registered.
|
|
||||||
### Removed
|
|
||||||
- Home page Notiwind test buttons
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.2] - 2023.11.01 - 7f6c93802911a030a89fe3706e18b5c17151e5bb
|
|
||||||
### Added
|
|
||||||
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
|
||||||
22
README.md
22
README.md
@@ -1,9 +1,6 @@
|
|||||||
# kickstart-for-time-pwa
|
# kickstart-for-time-pwa
|
||||||
|
|
||||||
## Project setup
|
## Project setup
|
||||||
|
|
||||||
We have pkgx.dev set up in package.json, so you can use `dev` to set up the dev environment.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
@@ -26,12 +23,6 @@ npm run build
|
|||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
### Web-push
|
|
||||||
|
|
||||||
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
|
|
||||||
|
|
||||||
### Test key contents
|
### Test key contents
|
||||||
|
|
||||||
See [this page](openssl_signing_console.rst)
|
See [this page](openssl_signing_console.rst)
|
||||||
@@ -54,8 +45,8 @@ by an existing user:
|
|||||||
On the test server, User #0 has rights to register others, so you can start
|
On the test server, User #0 has rights to register others, so you can start
|
||||||
playing one of two ways:
|
playing one of two ways:
|
||||||
|
|
||||||
- Import the keys for the test User `did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F` by importing this seed phrase:
|
- Import the keys for the test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` by importing this seed phrase:
|
||||||
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
`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).)
|
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
||||||
|
|
||||||
- Alternatively, register someone else under User #0 automatically:
|
- Alternatively, register someone else under User #0 automatically:
|
||||||
@@ -95,6 +86,15 @@ Clear cache for localhost, then go to http://localhost:8080/start
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
See https://tea.xyz
|
||||||
|
|
||||||
|
| Project | Version |
|
||||||
|
| ---------- | --------- |
|
||||||
|
| nodejs.org | ^16.0.0 |
|
||||||
|
| npmjs.com | ^8.0.0 |
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
### Reference Material
|
### Reference Material
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
JWT Creation & Verification
|
Prerequisites:
|
||||||
|
|
||||||
To run this in a script, see ./openssl_signing_console.sh
|
jq
|
||||||
|
|
||||||
Prerequisites: openssl, 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:
|
||||||
|
|
||||||
@@ -18,22 +15,20 @@ openssl ec -in private.pem -pubout -out public.pem
|
|||||||
|
|
||||||
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
||||||
|
|
||||||
Next, create a payload object as a JSON object containing the claims you want to include in the JWT.
|
Next, create a payload object as a JSON object containing the claims you want to include in the JWT. For example schema.org :
|
||||||
For example schema.org :
|
|
||||||
|
|
||||||
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
|
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
|
||||||
|
|
||||||
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
||||||
|
|
||||||
Concatenate the encoded header, payload, and a secret to create the signing input:
|
Concatenate the encoded header, payload, and a secret to create the signing input:
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
Create the signature by signing the signing input with a ES256K algorithm and your secret.
|
Create the signature by signing the signing input with a ES256K algorithm and your secret. You can use the openssl command line utility to do this:
|
||||||
You can use the openssl command line utility to do this:
|
|
||||||
|
|
||||||
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
||||||
|
|
||||||
@@ -48,7 +43,7 @@ Authorization: Bearer $jwt
|
|||||||
|
|
||||||
To verify the JWT, you can use the openssl utility with the public key:
|
To verify the JWT, you can use the openssl utility with the public key:
|
||||||
|
|
||||||
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
|
openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
|
||||||
|
|
||||||
|
This will verify the signature and output Verified OK if the signature is valid. If the signature is not valid, it will output an error.
|
||||||
|
|
||||||
This will verify the signature and output "Verified OK" if the signature is valid.
|
|
||||||
If the signature is not valid, it will give an error response and output "Verification failure".
|
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Generate a JWT, with signature verified using OpenSSL
|
|
||||||
#
|
|
||||||
# Prerequisites: openssl, jq
|
|
||||||
#
|
|
||||||
# Usage: source ./openssl_signing_console.sh
|
|
||||||
#
|
|
||||||
# For a more complete explanation, see ./openssl_signing_console.rst
|
|
||||||
#
|
|
||||||
# It's crazy that raw execution only works about 20% of the time!
|
|
||||||
# See https://stackoverflow.com/questions/77505582/why-would-openssl-verify-succeed-every-time-with-source-but-fail-80-of-the
|
|
||||||
|
|
||||||
|
|
||||||
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
||||||
openssl ec -in private.pem -pubout -out public.pem
|
openssl ec -in private.pem -pubout -out public.pem
|
||||||
|
|
||||||
@@ -19,25 +7,19 @@ header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
|||||||
|
|
||||||
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
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' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
|
||||||
|
|
||||||
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
|
# Read binary signature from file and encode it to Base64 URL-Safe format
|
||||||
|
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
# Also tested this, to no avail.
|
|
||||||
#echo -n "$signature" > sig.out
|
|
||||||
#echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature sig.out
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Read binary signature and encode it to Base64 URL-Safe format
|
|
||||||
signature_b64=$(echo -n "$signature" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
|
||||||
|
|
||||||
# Construct the JWT
|
# Construct the JWT
|
||||||
jwt="$signing_input.$signature_b64"
|
jwt="$signing_input.$signature_b64"
|
||||||
|
|
||||||
echo Resulting JWT: $jwt
|
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
26455
package-lock.json
generated
26455
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
70
package.json
70
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kickstart-for-time-pwa",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.1.3",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@@ -9,59 +9,59 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@tweenjs/tween.js": "^21.0.0",
|
"@tweenjs/tween.js": "^21.0.0",
|
||||||
"@veramo/core": "^5.4.1",
|
"@veramo/core": "^5.2.0",
|
||||||
"@veramo/credential-w3c": "^5.4.1",
|
"@veramo/credential-w3c": "^5.2.0",
|
||||||
"@veramo/data-store": "^5.4.1",
|
"@veramo/data-store": "^5.2.0",
|
||||||
"@veramo/did-manager": "^5.4.1",
|
"@veramo/did-manager": "^5.1.2",
|
||||||
"@veramo/did-provider-ethr": "^5.4.1",
|
"@veramo/did-provider-ethr": "^5.1.2",
|
||||||
"@veramo/did-resolver": "^5.4.1",
|
"@veramo/did-resolver": "^5.2.0",
|
||||||
"@veramo/key-manager": "^5.4.1",
|
"@veramo/key-manager": "^5.1.2",
|
||||||
"@vueuse/core": "^10.4.1",
|
"@vueuse/core": "^10.2.1",
|
||||||
"@zxing/text-encoding": "^0.9.0",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
"axios": "^1.5.0",
|
"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.32.1",
|
"core-js": "^3.31.1",
|
||||||
"dexie": "^3.2.4",
|
"dexie": "^3.2.4",
|
||||||
"dexie-export-import": "^4.0.7",
|
"dexie-export-import": "^4.0.7",
|
||||||
"did-jwt": "^7.2.7",
|
"did-jwt": "^7.2.4",
|
||||||
"ethereum-cryptography": "^2.1.2",
|
"ethereum-cryptography": "^2.0.0",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"ethr-did-resolver": "^8.1.2",
|
"ethr-did-resolver": "^8.0.0",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.2.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"localstorage-slim": "^2.5.0",
|
"localstorage-slim": "^2.4.0",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.3.0",
|
||||||
"merkletreejs": "^0.3.10",
|
"merkletreejs": "^0.3.10",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
"pinia-plugin-persistedstate": "^3.2.0",
|
"pinia-plugin-persistedstate": "^3.1.0",
|
||||||
"qr-code-generator-vue3": "^1.4.21",
|
"qr-code-generator-vue3": "^1.4.21",
|
||||||
"ramda": "^0.29.0",
|
"ramda": "^0.29.0",
|
||||||
"readable-stream": "^4.4.2",
|
"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.156.1",
|
"three": "^0.154.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.2",
|
"vue-facing-decorator": "^2.1.20",
|
||||||
"vue-qrcode-reader": "^5.4.1",
|
"vue-qrcode-reader": "^5.3.4",
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.3",
|
||||||
"web-did-resolver": "^2.0.27"
|
"web-did-resolver": "^2.0.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/leaflet": "^1.9.4",
|
"@types/leaflet": "^1.9.4",
|
||||||
"@types/ramda": "^0.29.3",
|
"@types/ramda": "^0.29.3",
|
||||||
"@types/three": "^0.155.1",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^6.6.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@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",
|
||||||
@@ -71,15 +71,15 @@
|
|||||||
"@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.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.48.0",
|
"eslint": "^8.44.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"postcss": "^8.4.29",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "~5.2.2"
|
"typescript": "~5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
- fix "any" warnings
|
||||||
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
|
- fix missing updateAllFeed in ContactGiftingView page
|
||||||
|
- check that Anonymous users jdenticon are nulls (not "Anonymous")
|
||||||
|
- 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 :
|
- 40 notifications :
|
||||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||||
|
|
||||||
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
- .2 Rename repo to crowd-sourcing...
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- 08 Scan QR code to import into contacts assignee:matthew
|
||||||
|
- SEE: https://github.com/gruhn/vue-qrcode-reader
|
||||||
|
|
||||||
|
- 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 :
|
- Home Feed & Quick Give screen :
|
||||||
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
||||||
@@ -14,35 +28,31 @@ tasks:
|
|||||||
|
|
||||||
- 24 Move to Vite assignee:matthew
|
- 24 Move to Vite assignee:matthew
|
||||||
|
|
||||||
- .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
- .5 Allow edit of a contact name (but not the DID)
|
||||||
- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView)
|
- .2 Edit Plan does not have icons across the bottom assignee-group:ui
|
||||||
- .5 Add infinite scroll to gifts on the home page
|
- .5 include the hash of the latest commit, and maybe a version
|
||||||
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
|
||||||
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
|
|
||||||
- .1 when creating a plan, select location and then make sure you can deselect on Android
|
|
||||||
- .5 add link to further project / people when a project pays ahead
|
- .5 add link to further project / people when a project pays ahead
|
||||||
- .5 add project ID to the URL of the project-view, to make a project publicly-accessible
|
- .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
|
- .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
|
- .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
|
- .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
|
- .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"
|
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||||
- .1 Make give description text box into something that expands as they type
|
- .1 change default server in app.ts
|
||||||
- .1 Make contact info specific to Time Safari - rather pointing at CommunityCred.org
|
|
||||||
|
|
||||||
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
- 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
|
- 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)
|
- 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
|
- .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 customize favicon assignee-group:ui
|
||||||
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
- .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
|
- 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
|
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
|
||||||
- .5 make a VC details page
|
|
||||||
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
|
||||||
- .5 include the hash of the latest commit on help page next to version
|
|
||||||
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
|
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
@@ -51,7 +61,6 @@ tasks:
|
|||||||
|
|
||||||
- stats v1 :
|
- stats v1 :
|
||||||
- 01 show numeric stats
|
- 01 show numeric stats
|
||||||
- 04 show different graphic for projects vs people on world
|
|
||||||
- 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
|
- .5 don't load another instance of a bush if it already exists
|
||||||
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
||||||
@@ -59,7 +68,6 @@ tasks:
|
|||||||
|
|
||||||
- Release Minimum Viable Product :
|
- Release Minimum Viable Product :
|
||||||
- 08 thorough testing for errors & edge cases
|
- 08 thorough testing for errors & edge cases
|
||||||
- 01 ensure ability to recover server remotely, and add redundant access
|
|
||||||
- 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.
|
||||||
@@ -68,21 +76,12 @@ tasks:
|
|||||||
- 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
|
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
||||||
|
|
||||||
- .5 show seed phrase in a QR code for transfer to another device
|
|
||||||
|
|
||||||
- 32 accept images for projects
|
|
||||||
- 32 accept images for contacts
|
|
||||||
|
|
||||||
- linking between projects or plans :
|
- linking between projects or plans :
|
||||||
- show total time given to & from a project
|
- show total time given to & from a project
|
||||||
- terminology:
|
- terminology:
|
||||||
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
- 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)
|
- 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)
|
||||||
|
|
||||||
- .5 add "back" button to all screens that aren't part of the bottom tray
|
|
||||||
- .5 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
|
|
||||||
- .5 Replace Gifted/Give in ContactsView with GiftedDialog
|
|
||||||
|
|
||||||
- Stats :
|
- Stats :
|
||||||
- 01 point out user's location on the world
|
- 01 point out user's location on the world
|
||||||
- 01 present a credential selected from the stats
|
- 01 present a credential selected from the stats
|
||||||
|
|||||||
177
sample.txt
177
sample.txt
@@ -1,177 +0,0 @@
|
|||||||
|
|
||||||
> 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
|
|
||||||
|
|
||||||
240
src/App.vue
240
src/App.vue
@@ -157,27 +157,22 @@
|
|||||||
>
|
>
|
||||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
<p class="text-lg mb-4">
|
<p class="text-lg mb-4">
|
||||||
Would you like to <b>turn on</b> notifications for this app?
|
Would you like to turn on notifications for this app?
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<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"
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
@click="
|
|
||||||
close(notification.id);
|
|
||||||
turnOnNotifications();
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
Turn on Notifications
|
Turn on Notifications
|
||||||
</button>
|
</button>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
@click="maybeLater(notification.id)"
|
@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"
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Maybe Later
|
Maybe Later
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="never(notification.id)"
|
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Never
|
Never
|
||||||
@@ -186,71 +181,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</Notification>
|
</Notification>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,168 +189,4 @@
|
|||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts"></script>
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class App extends Vue {
|
|
||||||
b64 = "";
|
|
||||||
mounted() {
|
|
||||||
axios
|
|
||||||
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid")
|
|
||||||
.then((response) => {
|
|
||||||
this.b64 = response.data.vapidKey;
|
|
||||||
console.log(this.b64);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("API error", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private askPermission(): Promise<NotificationPermission> {
|
|
||||||
// Check if Notifications are supported
|
|
||||||
if (!("Notification" in window)) {
|
|
||||||
alert("This browser does not support notifications.");
|
|
||||||
return Promise.reject("This browser does not support notifications.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check existing permissions
|
|
||||||
if (Notification.permission === "granted") {
|
|
||||||
return Promise.resolve("granted");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request permission
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const permissionResult = Notification.requestPermission((result) => {
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (permissionResult) {
|
|
||||||
permissionResult.then(resolve, reject);
|
|
||||||
}
|
|
||||||
}).then((permissionResult) => {
|
|
||||||
console.log("Permission result:", permissionResult);
|
|
||||||
|
|
||||||
if (permissionResult !== "granted") {
|
|
||||||
alert("We need notification permission to provide certain features.");
|
|
||||||
return Promise.reject("We weren't granted permission.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissionResult;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async turnOnNotifications() {
|
|
||||||
return this.askPermission()
|
|
||||||
.then((permission) => {
|
|
||||||
console.log("Permission granted:", permission);
|
|
||||||
|
|
||||||
// Call the function and handle promises
|
|
||||||
this.subscribeToPush()
|
|
||||||
.then(() => {
|
|
||||||
console.log("Subscribed successfully.");
|
|
||||||
// Assuming the subscription object is available
|
|
||||||
return navigator.serviceWorker.ready;
|
|
||||||
})
|
|
||||||
.then((registration) => {
|
|
||||||
// Fetch the existing subscription object from the registration
|
|
||||||
return registration.pushManager.getSubscription();
|
|
||||||
})
|
|
||||||
.then((subscription) => {
|
|
||||||
if (subscription) {
|
|
||||||
console.log(subscription);
|
|
||||||
return this.sendSubscriptionToServer(subscription);
|
|
||||||
} else {
|
|
||||||
throw new Error("Subscription object is not available.");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
console.log("Subscription data sent to server.");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(
|
|
||||||
"Subscription or server communication failed:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("An error occurred:", error);
|
|
||||||
// Handle error appropriately here
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to convert URL base64 to Uint8Array
|
|
||||||
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
||||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
||||||
const base64 = (base64String + padding)
|
|
||||||
.replace(/-/g, "+")
|
|
||||||
.replace(/_/g, "/");
|
|
||||||
const rawData = window.atob(base64);
|
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return outputArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The subscribeToPush method
|
|
||||||
private subscribeToPush(): Promise<void> {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
if ("serviceWorker" in navigator && "PushManager" in window) {
|
|
||||||
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
|
|
||||||
const options: PushSubscriptionOptions = {
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: applicationServerKey,
|
|
||||||
};
|
|
||||||
console.log(options);
|
|
||||||
|
|
||||||
navigator.serviceWorker.ready
|
|
||||||
.then((registration) => {
|
|
||||||
return registration.pushManager.subscribe(options);
|
|
||||||
})
|
|
||||||
.then((subscription) => {
|
|
||||||
console.log("Push subscription successful:", subscription);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Push subscription failed:", error, options);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const errorMsg = "Push messaging is not supported";
|
|
||||||
console.warn(errorMsg);
|
|
||||||
reject(new Error(errorMsg));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendSubscriptionToServer(
|
|
||||||
subscription: PushSubscription,
|
|
||||||
): Promise<void> {
|
|
||||||
console.log(subscription);
|
|
||||||
return fetch("/web-push/subscribe", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(subscription),
|
|
||||||
}).then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to send subscription to server");
|
|
||||||
}
|
|
||||||
console.log("Subscription sent to server successfully.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
never(ID: string) {
|
|
||||||
alert(ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeLater(ID: string) {
|
|
||||||
alert(ID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -5,26 +5,13 @@
|
|||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
import { toSvg } from "jdenticon";
|
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
|
@Component
|
||||||
export default class EntityIcon extends Vue {
|
export default class EntityIcon extends Vue {
|
||||||
@Prop entityId = "";
|
@Prop entityId = "";
|
||||||
@Prop iconSize = 0;
|
@Prop iconSize = 0;
|
||||||
|
|
||||||
generateIdenticon() {
|
generateIdenticon() {
|
||||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
const svgString = toSvg(this.entityId, this.iconSize);
|
||||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
|
||||||
return svgString;
|
return svgString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div v-if="visible" class="dialog-overlay">
|
<div v-if="visible" class="dialog-overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<h1 class="text-xl font-bold text-center mb-4">
|
<h1 class="text-xl font-bold text-center mb-4">
|
||||||
{{ message }} {{ giver?.name || "somebody not named" }}
|
{{ message }} {{ giver?.name || "somebody not specified" }}
|
||||||
</h1>
|
</h1>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -51,57 +51,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
|
||||||
import { createAndSubmitGive, GiverInputInfo } from "@/libs/endorserServer";
|
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer";
|
||||||
import { accountsDB, db } from "@/db/index";
|
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
group: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class GiftedDialog extends Vue {
|
export default class GiftedDialog extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
@Prop message = "";
|
@Prop message = "";
|
||||||
@Prop projectId = "";
|
|
||||||
|
|
||||||
activeDid = "";
|
|
||||||
apiServer = "";
|
|
||||||
|
|
||||||
giver?: GiverInputInfo;
|
giver?: GiverInputInfo;
|
||||||
description = "";
|
description = "";
|
||||||
hours = "0";
|
hours = "0";
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
|
||||||
this.apiServer = settings?.apiServer || "";
|
|
||||||
this.activeDid = settings?.activeDid || "";
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (err: any) {
|
|
||||||
console.log("Error retrieving settings from database:", err);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text:
|
|
||||||
err.message ||
|
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open(giver: GiverInputInfo) {
|
open(giver: GiverInputInfo) {
|
||||||
this.giver = giver;
|
this.giver = giver;
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
@@ -119,169 +80,27 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
@Emit("dialog-result")
|
||||||
|
confirm(): GiverOutputInfo {
|
||||||
|
const result = {
|
||||||
|
action: "confirm",
|
||||||
|
giver: this.giver,
|
||||||
|
hours: parseFloat(this.hours),
|
||||||
|
description: this.description,
|
||||||
|
};
|
||||||
this.close();
|
this.close();
|
||||||
this.description = "";
|
this.description = "";
|
||||||
this.giver = undefined;
|
this.giver = undefined;
|
||||||
this.hours = "0";
|
this.hours = "0";
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm() {
|
@Emit("dialog-result")
|
||||||
|
cancel(): GiverOutputInfo {
|
||||||
|
const result = { action: "cancel" };
|
||||||
this.close();
|
this.close();
|
||||||
this.$notify(
|
return result;
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "toast",
|
|
||||||
text: "Recording the give...",
|
|
||||||
title: "",
|
|
||||||
},
|
|
||||||
1000,
|
|
||||||
);
|
|
||||||
// this is asynchronous, but we don't need to wait for it to complete
|
|
||||||
this.recordGive(
|
|
||||||
this.giver?.did as string | undefined,
|
|
||||||
this.description,
|
|
||||||
parseFloat(this.hours),
|
|
||||||
).then(() => {
|
|
||||||
this.description = "";
|
|
||||||
this.giver = undefined;
|
|
||||||
this.hours = "0";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
|
||||||
await accountsDB.open();
|
|
||||||
const account = (await accountsDB.accounts
|
|
||||||
.where("did")
|
|
||||||
.equals(activeDid)
|
|
||||||
.first()) as Account;
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error(
|
|
||||||
"Attempted to load Give records for DID ${activeDid} but no identity was found",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @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,
|
|
||||||
this.projectId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
result.type === "error" ||
|
|
||||||
this.isGiveCreationError(result.response)
|
|
||||||
) {
|
|
||||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
|
||||||
console.log("Error with give creation result:", result);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: errorMessage || "There was an error creating the give.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Success",
|
|
||||||
text: "That gift was recorded.",
|
|
||||||
},
|
|
||||||
10000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log("Error with give recordation 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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result response "data" from the server
|
|
||||||
* @returns true if the result indicates an error
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
isGiveCreationError(result: any) {
|
|
||||||
return result.status !== 201 || result.data?.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
|
||||||
* @returns best guess at an error message
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
getGiveCreationErrorMessage(result: any) {
|
|
||||||
return (
|
|
||||||
result.error?.userMessage ||
|
|
||||||
result.error?.error ||
|
|
||||||
result.response?.data?.error?.message
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* See also ../libs/veramo/setup.ts
|
* See also ../libs/veramo/setup.ts
|
||||||
*/
|
*/
|
||||||
export enum AppString {
|
export enum AppString {
|
||||||
APP_NAME = "TimeSafari",
|
APP_NAME = "Time Safari",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
|
|||||||
@@ -9,39 +9,59 @@ import {
|
|||||||
} from "./tables/settings";
|
} from "./tables/settings";
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
|
|
||||||
// Define types for tables that hold sensitive and non-sensitive data
|
// a separate DB because the seed is super-sensitive data
|
||||||
type SensitiveTables = { accounts: Table<Account> };
|
type SensitiveTables = {
|
||||||
|
accounts: Table<Account>;
|
||||||
|
};
|
||||||
|
|
||||||
type NonsensitiveTables = {
|
type NonsensitiveTables = {
|
||||||
contacts: Table<Contact>;
|
contacts: Table<Contact>;
|
||||||
settings: Table<Settings>;
|
settings: Table<Settings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
/**
|
||||||
|
* In order to make the next line be acceptable, the program needs to have its linter suppress a rule:
|
||||||
|
* https://typescript-eslint.io/rules/no-unnecessary-type-constraint/
|
||||||
|
*
|
||||||
|
* and change *any* to *unknown*
|
||||||
|
*
|
||||||
|
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
|
||||||
|
*/
|
||||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||||
|
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||||
|
const SensitiveSchemas = Object.assign({}, AccountsSchema);
|
||||||
|
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||||
BaseDexie & T;
|
BaseDexie & T;
|
||||||
|
|
||||||
// Initialize Dexie databases for sensitive and non-sensitive data
|
|
||||||
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
|
||||||
const SensitiveSchemas = { ...AccountsSchema };
|
|
||||||
|
|
||||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema };
|
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
||||||
|
|
||||||
// Manage the encryption key. If not present in localStorage, create and store it.
|
/**
|
||||||
|
* Needed to enable a special webpack setting to allow *await* below:
|
||||||
|
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create password and place password in localStorage.
|
||||||
|
*
|
||||||
|
* 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();
|
||||||
if (!localStorage.getItem("secret")) localStorage.setItem("secret", secret);
|
|
||||||
|
|
||||||
// Apply encryption to the sensitive database using the secret key
|
if (localStorage.getItem("secret") == null) {
|
||||||
|
localStorage.setItem("secret", secret);
|
||||||
|
}
|
||||||
|
|
||||||
encrypted(accountsDB, { secretKey: secret });
|
encrypted(accountsDB, { secretKey: secret });
|
||||||
|
|
||||||
// Define the schema for our databases
|
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
|
|
||||||
db.version(1).stores(NonsensitiveSchemas);
|
db.version(1).stores(NonsensitiveSchemas);
|
||||||
|
|
||||||
// Event handler to initialize the non-sensitive database with default settings
|
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
|
||||||
db.on("populate", () => {
|
db.on("populate", function () {
|
||||||
|
// ensure there's an initial entry for settings
|
||||||
db.settings.add({
|
db.settings.add({
|
||||||
id: MASTER_SETTINGS_KEY,
|
id: MASTER_SETTINGS_KEY,
|
||||||
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
||||||
|
|||||||
@@ -1,50 +1,17 @@
|
|||||||
/**
|
|
||||||
* 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 of type IIdentifier
|
||||||
/**
|
// https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
|
||||||
* 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
|
||||||
* Schema for the accounts table in the database.
|
// see https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon
|
||||||
* 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,47 +1,28 @@
|
|||||||
/**
|
|
||||||
* BoundingBox type describes the geographical bounding box coordinates.
|
|
||||||
*/
|
|
||||||
export type BoundingBox = {
|
export type BoundingBox = {
|
||||||
eastLong: number; // Eastern longitude
|
eastLong: number;
|
||||||
maxLat: number; // Maximum (Northernmost) latitude
|
maxLat: number;
|
||||||
minLat: number; // Minimum (Southernmost) latitude
|
minLat: number;
|
||||||
westLong: number; // Western longitude
|
westLong: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// a singleton
|
||||||
* Settings type encompasses user-specific configuration details.
|
|
||||||
*/
|
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
id: number; // Only one entry using MASTER_SETTINGS_KEY
|
id: number; // there's only one entry: MASTER_SETTINGS_KEY
|
||||||
activeDid?: string; // Active Decentralized ID
|
|
||||||
apiServer?: string; // API server URL
|
|
||||||
firstName?: string; // User's first name
|
|
||||||
lastName?: string; // User's last name
|
|
||||||
lastViewedClaimId?: string; // Last viewed claim ID
|
|
||||||
lastNotifiedClaimId?: string; // Last notified claim ID
|
|
||||||
isRegistered?: boolean;
|
|
||||||
|
|
||||||
// Array of named search boxes defined by bounding boxes
|
|
||||||
|
|
||||||
|
activeDid?: string;
|
||||||
|
apiServer?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
lastViewedClaimId?: string;
|
||||||
searchBoxes?: Array<{
|
searchBoxes?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
bbox: BoundingBox;
|
bbox: BoundingBox;
|
||||||
}>;
|
}>;
|
||||||
|
showContactGivesInline?: boolean;
|
||||||
showContactGivesInline?: boolean; // Display contact inline or not
|
|
||||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
|
||||||
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
|
||||||
reminderOn?: boolean; // Toggle to enable or disable reminders
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Schema for the Settings table in the database.
|
|
||||||
*/
|
|
||||||
export const SettingsSchema = {
|
export const SettingsSchema = {
|
||||||
settings: "id",
|
settings: "id",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Constants.
|
|
||||||
*/
|
|
||||||
export const MASTER_SETTINGS_KEY = 1;
|
export const MASTER_SETTINGS_KEY = 1;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import * as u8a from "uint8arrays";
|
|||||||
import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer";
|
import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer";
|
||||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
||||||
|
|
||||||
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
export const DEFAULT_ROOT_DERIVATION_PATH = "m/76798669'/0'/0'/0'";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -116,26 +116,28 @@ export function isHiddenDid(did: string) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
||||||
|
|
||||||
Similar logic is found in endorser-mobile.
|
|
||||||
**/
|
**/
|
||||||
export function didInfo(
|
export function didInfo(
|
||||||
did: string,
|
did: string,
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
allMyDids: string[],
|
allMyDids: Array<string>,
|
||||||
contacts: Contact[],
|
contacts: Array<Contact>,
|
||||||
): string {
|
): string {
|
||||||
if (!did) return "Someone Anonymous";
|
const myId: string | undefined = R.find(R.equals(did), allMyDids);
|
||||||
|
if (myId) {
|
||||||
const myId = R.find(R.equals(did), allMyDids);
|
return "You" + (myId !== activeDid ? " (Alt ID)" : "");
|
||||||
if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`;
|
} else {
|
||||||
|
const contact: Contact | undefined = R.find((c) => c.did === did, contacts);
|
||||||
const contact = R.find((c) => c.did === did, contacts);
|
if (contact) {
|
||||||
return contact
|
return contact.name || "Someone Unnamed in Contacts";
|
||||||
? contact.name || "Contact With No Name"
|
} else if (!did) {
|
||||||
: isHiddenDid(did)
|
return "Unspecified Person";
|
||||||
? "Someone Not In Network"
|
} else if (isHiddenDid(did)) {
|
||||||
: "Someone Not In Contacts";
|
return "Someone Not In Network";
|
||||||
|
} else {
|
||||||
|
return "Someone Not In Contacts";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultWithType {
|
export interface ResultWithType {
|
||||||
@@ -174,18 +176,30 @@ export async function createAndSubmitGive(
|
|||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
): Promise<CreateAndSubmitGiveResult> {
|
): Promise<CreateAndSubmitGiveResult> {
|
||||||
try {
|
try {
|
||||||
|
// Make a claim
|
||||||
const vcClaim: GiveVerifiableCredential = {
|
const vcClaim: GiveVerifiableCredential = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "GiveAction",
|
"@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,
|
|
||||||
};
|
};
|
||||||
|
if (toDid) {
|
||||||
|
vcClaim.recipient = { identifier: toDid };
|
||||||
|
}
|
||||||
|
if (fromDid) {
|
||||||
|
vcClaim.agent = { identifier: fromDid };
|
||||||
|
}
|
||||||
|
if (description) {
|
||||||
|
vcClaim.description = description;
|
||||||
|
}
|
||||||
|
if (hours) {
|
||||||
|
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
|
||||||
|
}
|
||||||
|
if (fulfillsProjectHandleId) {
|
||||||
|
vcClaim.fulfills = {
|
||||||
|
"@type": "PlanAction",
|
||||||
|
identifier: fulfillsProjectHandleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||||
@@ -196,7 +210,14 @@ export async function createAndSubmitGive(
|
|||||||
|
|
||||||
// Create a signature using private key of identity
|
// Create a signature using private key of identity
|
||||||
const firstKey = identity.keys[0];
|
const firstKey = identity.keys[0];
|
||||||
const privateKeyHex = firstKey?.privateKeyHex;
|
if (!firstKey || !firstKey.privateKeyHex) {
|
||||||
|
throw {
|
||||||
|
error: "No private key",
|
||||||
|
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKeyHex = firstKey.privateKeyHex;
|
||||||
|
|
||||||
if (!privateKeyHex) {
|
if (!privateKeyHex) {
|
||||||
throw {
|
throw {
|
||||||
@@ -206,35 +227,48 @@ export async function createAndSubmitGive(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const signer = await SimpleSigner(privateKeyHex);
|
const signer = await SimpleSigner(privateKeyHex);
|
||||||
|
const alg = undefined;
|
||||||
|
|
||||||
// Create a JWT for the request
|
// Create a JWT for the request
|
||||||
|
|
||||||
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||||
|
alg: alg,
|
||||||
issuer: identity.did,
|
issuer: identity.did,
|
||||||
signer,
|
signer: signer,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make the xhr request payload
|
// Make the xhr request payload
|
||||||
|
|
||||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||||
const url = `${apiServer}/api/v2/claim`;
|
const url = apiServer + "/api/v2/claim";
|
||||||
const token = await accessToken(identity);
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
|
||||||
const response = await axios.post(url, payload, {
|
const response = await axios.post(url, payload, { headers });
|
||||||
headers: {
|
return {
|
||||||
"Content-Type": "application/json",
|
type: "success",
|
||||||
Authorization: `Bearer ${token}`,
|
response,
|
||||||
},
|
};
|
||||||
});
|
|
||||||
|
|
||||||
return { type: "success", response };
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage: string =
|
let errorMessage: string;
|
||||||
error === null
|
|
||||||
? "Null error"
|
if (error instanceof Error) {
|
||||||
: error instanceof Error
|
// If it's a JavaScript Error object
|
||||||
? error.message
|
errorMessage = error.message;
|
||||||
: typeof error === "object" && error !== null && "message" in error
|
} else if (
|
||||||
? (error as { message: string }).message
|
typeof error === "object" &&
|
||||||
: "Unknown error";
|
error !== null &&
|
||||||
|
"message" in error
|
||||||
|
) {
|
||||||
|
// If it's an object that has a 'message' property
|
||||||
|
errorMessage = (error as { message: string }).message;
|
||||||
|
} else {
|
||||||
|
// Unknown error shape, default message
|
||||||
|
errorMessage = "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
|||||||
import {
|
import {
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faBan,
|
|
||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
@@ -60,7 +59,6 @@ import {
|
|||||||
library.add(
|
library.add(
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faBan,
|
|
||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "home",
|
name: "home",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
|
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
|
||||||
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/account",
|
path: "/account",
|
||||||
@@ -57,14 +58,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
|
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/contact-gives",
|
|
||||||
name: "contact-gives",
|
|
||||||
component: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/contact-qr",
|
path: "/contact-qr",
|
||||||
name: "contact-qr",
|
name: "contact-qr",
|
||||||
@@ -78,6 +71,15 @@ 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",
|
||||||
|
name: "scan-contact",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/discover",
|
path: "/discover",
|
||||||
@@ -91,14 +93,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
|
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/identity-switcher",
|
|
||||||
name: "identity-switcher",
|
|
||||||
component: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/import-account",
|
path: "/import-account",
|
||||||
name: "import-account",
|
name: "import-account",
|
||||||
@@ -148,7 +142,15 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/project/:id?",
|
path: "/identity-switcher",
|
||||||
|
name: "identity-switcher",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/project",
|
||||||
name: "project",
|
name: "project",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
||||||
@@ -160,22 +162,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
|
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
|
||||||
beforeEnter: enterOrStart,
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/scan-contact",
|
|
||||||
name: "scan-contact",
|
|
||||||
component: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/search-area",
|
|
||||||
name: "search-area",
|
|
||||||
component: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "search-area" */ "../views/SearchAreaView.vue"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/seed-backup",
|
path: "/seed-backup",
|
||||||
name: "seed-backup",
|
name: "seed-backup",
|
||||||
@@ -199,10 +185,12 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/test",
|
path: "/contact-gives",
|
||||||
name: "test",
|
name: "contact-gives",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "test" */ "../views/TestView.vue"),
|
import(
|
||||||
|
/* webpackChunkName: "statistics" */ "../views/ContactGiftingView.vue"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -52,17 +52,7 @@
|
|||||||
|
|
||||||
<!-- Identity Details -->
|
<!-- Identity Details -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
<h2 class="text-xl font-semibold mb-2">{{ firstName }} {{ lastName }}</h2>
|
||||||
{{ givenName }}
|
|
||||||
</h2>
|
|
||||||
<span v-else>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'new-edit-account' }"
|
|
||||||
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
|
||||||
>
|
|
||||||
(set name)
|
|
||||||
</router-link>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">ID</div>
|
<div class="text-slate-500 text-sm font-bold">ID</div>
|
||||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||||
@@ -77,75 +67,70 @@
|
|||||||
</button>
|
</button>
|
||||||
<span v-show="showDidCopy">Copied!</span>
|
<span v-show="showDidCopy">Copied!</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
||||||
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||||
|
<code class="truncate">{{ publicBase64 }}</code>
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
||||||
|
"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
<span v-show="showB64Copy">Copied!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
||||||
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||||
|
<code class="truncate">{{ publicHex }}</code>
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
||||||
|
"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
<span v-show="showPubCopy">Copied!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
||||||
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||||
|
<code class="truncate">{{ derivationPath }}</code>
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
doCopyTwoSecRedo(derivationPath, () => (showDerCopy = !showDerCopy))
|
||||||
|
"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
<span v-show="showDerCopy">Copied!</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'new-edit-account' }"
|
:to="{ name: 'new-edit-account' }"
|
||||||
class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
|
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>
|
||||||
|
<button
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
@click="
|
||||||
<label
|
this.$notify(
|
||||||
for="toggleNotifications"
|
{
|
||||||
class="flex items-center cursor-pointer"
|
group: 'modal',
|
||||||
@click="
|
type: 'notification-permission',
|
||||||
this.$notify(
|
},
|
||||||
{
|
-1,
|
||||||
group: 'modal',
|
)
|
||||||
type: 'notification-permission',
|
"
|
||||||
},
|
class="block w-full text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
|
||||||
-1,
|
>
|
||||||
)
|
Turn on Notifications
|
||||||
"
|
</button>
|
||||||
>
|
|
||||||
<!-- label -->
|
|
||||||
<div>App Notifications</div>
|
|
||||||
<!-- toggle -->
|
|
||||||
<div class="relative ml-2">
|
|
||||||
<!-- 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>
|
|
||||||
<label
|
|
||||||
for="toggleMuteNotifications"
|
|
||||||
class="flex items-center cursor-pointer mt-4"
|
|
||||||
@click="
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: 'modal',
|
|
||||||
type: 'notification-mute',
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<!-- label -->
|
|
||||||
<div>Mute Notifications</div>
|
|
||||||
<!-- toggle -->
|
|
||||||
<div class="relative ml-2">
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||||
|
|
||||||
@@ -160,13 +145,32 @@
|
|||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
||||||
@click="exportDatabase()"
|
@click="exportDatabase()"
|
||||||
>
|
>
|
||||||
Download Settings & Contacts
|
Download Settings & Contacts (excluding Identifier Data)
|
||||||
<br />
|
|
||||||
(excluding Identifier Data)
|
|
||||||
</a>
|
</a>
|
||||||
<a ref="downloadLink" />
|
<a ref="downloadLink" />
|
||||||
|
|
||||||
<div v-if="activeDid" class="flex py-2">
|
<!-- QR code popup -->
|
||||||
|
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
|
||||||
|
<div class="text-slate-500 text-center">
|
||||||
|
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
||||||
|
</div>
|
||||||
|
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
value="cancel"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Copy to Clipboard
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
value="cancel"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<div class="flex py-2">
|
||||||
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
||||||
Check Limits
|
Check Limits
|
||||||
</button>
|
</button>
|
||||||
@@ -193,84 +197,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
|
||||||
<h3
|
<h3
|
||||||
id="advanced"
|
|
||||||
class="text-sm uppercase font-semibold mb-3"
|
class="text-sm uppercase font-semibold mb-3"
|
||||||
@click="showAdvanced = !showAdvanced"
|
@click="showAdvanced = !showAdvanced"
|
||||||
>
|
>
|
||||||
Advanced
|
Advanced
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div v-if="showAdvanced">
|
<div v-if="showAdvanced">
|
||||||
<!-- Deep Identity Details -->
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
|
|
||||||
Deep Identity Details
|
|
||||||
</h2>
|
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
|
||||||
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
|
||||||
<div
|
|
||||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
|
||||||
>
|
|
||||||
<code class="truncate">{{ publicBase64 }}</code>
|
|
||||||
<button
|
|
||||||
@click="
|
|
||||||
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
|
||||||
"
|
|
||||||
class="ml-2"
|
|
||||||
>
|
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
||||||
</button>
|
|
||||||
<span v-show="showB64Copy">Copied!</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
|
||||||
<div
|
|
||||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
|
||||||
>
|
|
||||||
<code class="truncate">{{ publicHex }}</code>
|
|
||||||
<button
|
|
||||||
@click="
|
|
||||||
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
|
||||||
"
|
|
||||||
class="ml-2"
|
|
||||||
>
|
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
||||||
</button>
|
|
||||||
<span v-show="showPubCopy">Copied!</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
|
||||||
<div
|
|
||||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
|
||||||
>
|
|
||||||
<code class="truncate">{{ derivationPath }}</code>
|
|
||||||
<button
|
|
||||||
@click="
|
|
||||||
doCopyTwoSecRedo(
|
|
||||||
derivationPath,
|
|
||||||
() => (showDerCopy = !showDerCopy),
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="ml-2"
|
|
||||||
>
|
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
||||||
</button>
|
|
||||||
<span v-show="showDerCopy">Copied!</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label
|
<label
|
||||||
for="toggleShowAmounts"
|
for="toggleShowAmounts"
|
||||||
class="flex items-center cursor-pointer py-2"
|
class="flex items-center cursor-pointer mb-6"
|
||||||
@click="handleChange"
|
@click="handleChange"
|
||||||
>
|
>
|
||||||
<!-- label -->
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
|
||||||
Show amounts given with contacts
|
|
||||||
</h2>
|
|
||||||
<!-- toggle -->
|
<!-- toggle -->
|
||||||
<div class="relative ml-2">
|
<div class="relative">
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -285,31 +225,21 @@
|
|||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- label -->
|
||||||
|
<div class="ml-2">Show amounts given with contacts</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
<button class="text-blue-500">
|
<router-link
|
||||||
<!-- id used by puppeteer test script -->
|
:to="{ name: 'identity-switcher' }"
|
||||||
<router-link
|
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
id="switch-identity-link"
|
>
|
||||||
:to="{ name: 'identity-switcher' }"
|
Switch Identity / No Identity
|
||||||
class="block text-center"
|
</router-link>
|
||||||
>
|
|
||||||
Switch Identity / No Identity
|
|
||||||
</router-link>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
<button class="text-blue-500">
|
Claim Server
|
||||||
<router-link :to="{ name: 'statistics' }" class="block text-center">
|
|
||||||
See Achievements & Statistics
|
|
||||||
</router-link>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex py-4">
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||||
@@ -341,21 +271,32 @@
|
|||||||
Use Local
|
Use Local
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="text-blue-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'statistics' }"
|
||||||
|
class="block text-center py-3"
|
||||||
|
>
|
||||||
|
See Achievements & Statistics
|
||||||
|
</router-link>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios/index";
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
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 QuickNav from "@/components/QuickNav.vue";
|
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
||||||
|
|
||||||
@@ -369,13 +310,6 @@ interface Notification {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAccount {
|
|
||||||
did: string;
|
|
||||||
publicKeyHex: string;
|
|
||||||
privateHex?: string;
|
|
||||||
derivationPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
export default class AccountViewView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -386,8 +320,8 @@ export default class AccountViewView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
apiServerInput = "";
|
apiServerInput = "";
|
||||||
derivationPath = "";
|
derivationPath = "";
|
||||||
givenName = "";
|
firstName = "";
|
||||||
isRegistered = false;
|
lastName = "";
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
publicBase64 = "";
|
publicBase64 = "";
|
||||||
@@ -402,57 +336,26 @@ export default class AccountViewView extends Vue {
|
|||||||
showPubCopy = false;
|
showPubCopy = false;
|
||||||
|
|
||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
|
alertMessage = "";
|
||||||
|
alertTitle = "";
|
||||||
|
|
||||||
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
public async getIdentity(activeDid: string) {
|
||||||
try {
|
await accountsDB.open();
|
||||||
// Open the accounts database
|
const account = await accountsDB.accounts
|
||||||
await accountsDB.open();
|
.where("did")
|
||||||
} catch (error) {
|
.equals(activeDid)
|
||||||
console.error("Failed to open accounts database:", error);
|
.first();
|
||||||
return null;
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
}
|
return identity;
|
||||||
|
|
||||||
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 as string) || "null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public async getHeaders(identity: IIdentifier) {
|
||||||
* Asynchronously retrieves headers for HTTP requests.
|
const token = await accessToken(identity);
|
||||||
*
|
const headers = {
|
||||||
* @param {IIdentifier} identity - The identity object for which to generate the headers.
|
"Content-Type": "application/json",
|
||||||
* @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
|
Authorization: "Bearer " + token,
|
||||||
*
|
};
|
||||||
* @throws Will throw an error if unable to generate an access token.
|
return headers;
|
||||||
*/
|
|
||||||
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
|
||||||
@@ -477,94 +380,60 @@ export default class AccountViewView extends Vue {
|
|||||||
this.numAccounts = await accountsDB.accounts.count();
|
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 this 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);
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.apiServerInput = settings?.apiServer || "";
|
||||||
|
this.firstName = settings?.firstName || "";
|
||||||
|
this.lastName = settings?.lastName || "";
|
||||||
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
// 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);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
|
||||||
if (identity) {
|
if (identity) {
|
||||||
this.processIdentity(identity);
|
this.publicHex = identity.keys[0].publicKeyHex;
|
||||||
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString(
|
||||||
|
"base64",
|
||||||
|
);
|
||||||
|
this.derivationPath = identity.keys[0].meta.derivationPath;
|
||||||
|
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: identity.did,
|
||||||
|
});
|
||||||
|
this.checkLimitsFor(identity);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
this.handleError(err);
|
} catch (err: any) {
|
||||||
}
|
if (
|
||||||
}
|
err.message ===
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes component state with values from the database or defaults.
|
|
||||||
* @param {SettingsType} settings - Object containing settings from the database.
|
|
||||||
*/
|
|
||||||
initializeState(settings: Settings | undefined) {
|
|
||||||
this.activeDid = (settings?.activeDid as string) || "";
|
|
||||||
this.apiServer = (settings?.apiServer as string) || "";
|
|
||||||
this.apiServerInput = (settings?.apiServer as string) || "";
|
|
||||||
this.givenName =
|
|
||||||
(settings?.firstName || "") +
|
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes the identity and updates the component's state.
|
|
||||||
* @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.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
|
||||||
this.derivationPath = identity.keys[0].meta.derivationPath as string;
|
|
||||||
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
activeDid: identity.did,
|
|
||||||
});
|
|
||||||
this.checkLimitsFor(identity);
|
|
||||||
} else {
|
|
||||||
// Handle the case where any of these are null or undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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."
|
"Attempted to load account records with no identity available."
|
||||||
) {
|
) {
|
||||||
this.limitsMessage = "No identity.";
|
this.limitsMessage = "No identity.";
|
||||||
this.loadingLimits = false;
|
this.loadingLimits = false;
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Creating Account",
|
title: "Error Creating Account",
|
||||||
text: "Clear your cache and start over (after data backup).",
|
text: "Clear your cache and start over (after data backup).",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error("Telling user to clear cache at page create because:", err);
|
console.error(
|
||||||
|
"Telling user to clear cache at page create because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,96 +460,41 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
||||||
// Generate the blob from the database
|
const blob = await db.export({ prettyJson: true });
|
||||||
const blob = await this.generateDatabaseBlob();
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
// Create a temporary URL for the blob
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||||
const url = this.createBlobURL(blob);
|
downloadAnchor.href = url;
|
||||||
|
downloadAnchor.download = db.name + "-backup.json";
|
||||||
|
downloadAnchor.click();
|
||||||
|
|
||||||
// Trigger the download
|
|
||||||
this.downloadDatabaseBackup(url);
|
|
||||||
|
|
||||||
// Revoke the temporary URL
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
// Notify the user that the download has started
|
this.$notify(
|
||||||
this.notifyDownloadStarted();
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Download Started",
|
||||||
|
text: "See your downloads directory for the backup.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleExportError(error);
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Export Error",
|
||||||
|
text: "See console logs for more info.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
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: "success",
|
|
||||||
title: "Download Started",
|
|
||||||
text: "See your downloads directory for the backup.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
if (identity) {
|
if (identity) {
|
||||||
@@ -688,146 +502,68 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async checkLimitsFor(identity: IIdentifier) {
|
||||||
* Asynchronously checks rate limits for the given identity.
|
|
||||||
*
|
|
||||||
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
|
|
||||||
*/
|
|
||||||
public async checkLimitsFor(identity: IIdentifier) {
|
|
||||||
this.loadingLimits = true;
|
this.loadingLimits = true;
|
||||||
this.limitsMessage = "";
|
this.limitsMessage = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.fetchRateLimits(identity);
|
const url = this.apiServer + "/api/report/rateLimits";
|
||||||
|
const headers = await this.getHeaders(identity);
|
||||||
|
|
||||||
|
const resp = await this.axios.get(url, { headers });
|
||||||
|
// axios throws an exception on a 400
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.limits = resp.data;
|
this.limits = resp.data;
|
||||||
if (!this.isRegistered) {
|
|
||||||
// the user is not known to be registered, but they are so let's record it
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
isRegistered: true,
|
|
||||||
});
|
|
||||||
this.isRegistered = true;
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Got an error updating settings:", err);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "Update Error",
|
|
||||||
text: "Unable to update your settings. Check claim limits again.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
this.handleRateLimitsError(error);
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
error.message ===
|
||||||
|
"Attempted to load Give records with no identity available."
|
||||||
|
) {
|
||||||
|
this.limitsMessage = "No identity.";
|
||||||
|
this.loadingLimits = false;
|
||||||
|
} else {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
console.error("Bad response retrieving limits: ", serverError);
|
||||||
|
|
||||||
|
const data = (serverError.response &&
|
||||||
|
serverError.response.data) as ErrorResponse;
|
||||||
|
this.limitsMessage = data?.error?.message || "Bad server response.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingLimits = false;
|
this.loadingLimits = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async switchAccount(accountNum: number) {
|
||||||
* Fetches rate limits from the server.
|
// 0 means none
|
||||||
*
|
|
||||||
* @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 as string) || "Bad server response.";
|
|
||||||
console.log(
|
|
||||||
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
|
|
||||||
this.limitsMessage,
|
|
||||||
//error,
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
error instanceof Error &&
|
|
||||||
error.message ===
|
|
||||||
"Attempted to load Give records with no identity available."
|
|
||||||
) {
|
|
||||||
this.limitsMessage = "No identity.";
|
|
||||||
} else {
|
|
||||||
// Handle other unknown errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
if (accountNum === 0) {
|
||||||
this.switchToNoAccount();
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: undefined,
|
||||||
|
});
|
||||||
|
this.activeDid = "";
|
||||||
|
this.derivationPath = "";
|
||||||
|
this.publicHex = "";
|
||||||
|
this.publicBase64 = "";
|
||||||
} else {
|
} else {
|
||||||
await this.switchToAccountNumber(accountNum);
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = accounts[accountNum - 1];
|
||||||
|
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: account.did,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeDid = account.did;
|
||||||
|
this.derivationPath = account.derivationPath;
|
||||||
|
this.publicHex = account.publicKeyHex;
|
||||||
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
const account = accounts[accountNum - 1];
|
|
||||||
|
|
||||||
await 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.derivationPath = account.derivationPath;
|
|
||||||
this.publicHex = account.publicKeyHex;
|
|
||||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
|
||||||
}
|
|
||||||
|
|
||||||
public showContactGivesClassNames() {
|
public showContactGivesClassNames() {
|
||||||
return {
|
return {
|
||||||
"bg-slate-900": !this.showContactGives,
|
"bg-slate-900": !this.showContactGives,
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
<QuickNav selected="Contacts"></QuickNav>
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
id="ViewBreadcrumb"
|
|
||||||
class="text-lg text-center font-light relative px-7"
|
|
||||||
>
|
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contacts' }"
|
:to="{ name: 'contacts' }"
|
||||||
@@ -14,12 +11,11 @@
|
|||||||
><fa icon="chevron-left" class="fa-fw"></fa
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
></router-link>
|
></router-link>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
|
||||||
Given with {{ contact?.name }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Given with {{ contact?.name }}
|
||||||
|
</h1>
|
||||||
<div class="flex justify-around">
|
<div class="flex justify-around">
|
||||||
<span />
|
<span />
|
||||||
<span class="justify-around">(Only 50 most recent)</span>
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
@@ -362,10 +358,7 @@ export default class ContactsView extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/*
|
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
||||||
Tooltip, generated on "title" attributes on "fa" icons
|
|
||||||
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
|
||||||
*/
|
|
||||||
/* Tooltip container */
|
/* Tooltip container */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -16,15 +16,19 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Search -->
|
||||||
|
|
||||||
|
<!-- Initial Loading Animation -->
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul class="border-t border-slate-300">
|
<ul class="border-t border-slate-300">
|
||||||
<li class="border-b border-slate-300 py-3">
|
<li class="border-b border-slate-300 py-3">
|
||||||
<h2 class="text-base flex gap-4 items-center">
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
<span class="grow italic text-slate-500"
|
<span class="grow italic text-slate-500"
|
||||||
><EntityIcon
|
><EntityIcon
|
||||||
:entityId="null"
|
entityId="Anonymous"
|
||||||
:iconSize="32"
|
:iconSize="32"
|
||||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
class="opacity-50 inline-block align-middle border border-dashed border-slate-400 bg-slate-200 rounded-md mr-1"
|
||||||
></EntityIcon>
|
></EntityIcon>
|
||||||
Anonymous
|
Anonymous
|
||||||
</span>
|
</span>
|
||||||
@@ -66,7 +70,12 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
|
<GiftedDialog
|
||||||
|
ref="customDialog"
|
||||||
|
@dialog-result="handleDialogResult"
|
||||||
|
message="Received from"
|
||||||
|
>
|
||||||
|
</GiftedDialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -74,10 +83,16 @@
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { Account, AccountsSchema } from "@/db/tables/accounts";
|
import { AccountsSchema } from "@/db/tables/accounts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { GiverInputInfo } from "@/libs/endorserServer";
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
|
CreateAndSubmitGiveResult,
|
||||||
|
ErrorResult,
|
||||||
|
GiverInputInfo,
|
||||||
|
GiverOutputInfo,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
@@ -93,7 +108,7 @@ interface Notification {
|
|||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ContactGiftingView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
@@ -109,10 +124,10 @@ export default class ContactGiftingView extends Vue {
|
|||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const account = (await accountsDB.accounts
|
const account = await accountsDB.accounts
|
||||||
.where("did")
|
.where("did")
|
||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first()) as Account;
|
.first();
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@@ -135,7 +150,7 @@ export default class ContactGiftingView extends Vue {
|
|||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
@@ -158,5 +173,123 @@ export default class ContactGiftingView extends Vue {
|
|||||||
openDialog(giver: GiverInputInfo) {
|
openDialog(giver: GiverInputInfo) {
|
||||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
(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>
|
</script>
|
||||||
|
|||||||
@@ -108,9 +108,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
iat: Date.now(),
|
iat: Date.now(),
|
||||||
iss: this.activeDid,
|
iss: this.activeDid,
|
||||||
own: {
|
own: {
|
||||||
name:
|
name: (settings?.firstName || "") + " " + (settings?.lastName || ""),
|
||||||
(settings?.firstName || "") +
|
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
|
||||||
publicEncKey,
|
publicEncKey,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,16 +93,6 @@
|
|||||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||||
></EntityIcon>
|
></EntityIcon>
|
||||||
{{ contact.name || "(no name)" }}
|
{{ contact.name || "(no name)" }}
|
||||||
<button
|
|
||||||
class="text-sm uppercase bg-slate-500 text-white px-1 rounded-md"
|
|
||||||
@click="
|
|
||||||
contactEdit = contact;
|
|
||||||
contactNewName = contact.name;
|
|
||||||
"
|
|
||||||
title="Edit"
|
|
||||||
>
|
|
||||||
<fa icon="pen" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-sm truncate">{{ contact.did }}</div>
|
<div class="text-sm truncate">{{ contact.did }}</div>
|
||||||
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
||||||
@@ -218,32 +208,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else>There are no contacts.</p>
|
<p v-else>This identity has no contacts.</p>
|
||||||
|
|
||||||
<div v-if="contactEdit !== null" class="dialog-overlay">
|
|
||||||
<div class="dialog">
|
|
||||||
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
||||||
placeholder="Name"
|
|
||||||
v-model="contactNewName"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
|
||||||
@click="onClickSaveName(contactEdit, contactNewName)"
|
|
||||||
>
|
|
||||||
<fa icon="save" />
|
|
||||||
</button>
|
|
||||||
<span class="inline-block w-2" />
|
|
||||||
<button
|
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
|
||||||
@click="onClickCancelName()"
|
|
||||||
>
|
|
||||||
<fa icon="ban" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -256,7 +221,7 @@ import { NotificationIface } from "@/constants/app";
|
|||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import {
|
import {
|
||||||
accessToken,
|
accessToken,
|
||||||
getContactPayloadFromJwtUrl,
|
getContactPayloadFromJwtUrl,
|
||||||
@@ -271,7 +236,6 @@ import {
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.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;
|
||||||
@@ -287,8 +251,6 @@ export default class ContactsView extends Vue {
|
|||||||
contacts: Array<Contact> = [];
|
contacts: Array<Contact> = [];
|
||||||
contactEndorserUrl = localStorage.getItem("contactEndorserUrl") || "";
|
contactEndorserUrl = localStorage.getItem("contactEndorserUrl") || "";
|
||||||
contactInput = "";
|
contactInput = "";
|
||||||
contactEdit: Contact | null = null;
|
|
||||||
contactNewName = "";
|
|
||||||
// { "did:...": concatenated-descriptions } entry for each contact
|
// { "did:...": concatenated-descriptions } entry for each contact
|
||||||
givenByMeDescriptions: Record<string, string> = {};
|
givenByMeDescriptions: Record<string, string> = {};
|
||||||
// { "did:...": amount } entry for each contact
|
// { "did:...": amount } entry for each contact
|
||||||
@@ -309,7 +271,7 @@ export default class ContactsView extends Vue {
|
|||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
|
||||||
@@ -333,7 +295,7 @@ export default class ContactsView extends Vue {
|
|||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
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) as Account;
|
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@@ -395,7 +357,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error With Server",
|
||||||
text:
|
text:
|
||||||
"Got an error retrieving your " +
|
"Got an error retrieving your " +
|
||||||
(useRecipient ? "given" : "received") +
|
(useRecipient ? "given" : "received") +
|
||||||
@@ -454,7 +416,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error With Server",
|
||||||
text: error as string,
|
text: error as string,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
@@ -590,16 +552,6 @@ export default class ContactsView extends Vue {
|
|||||||
"?",
|
"?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "toast",
|
|
||||||
text: "",
|
|
||||||
title: "Registration submitted...",
|
|
||||||
},
|
|
||||||
1000,
|
|
||||||
);
|
|
||||||
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
|
||||||
const vcClaim: RegisterVerifiableCredential = {
|
const vcClaim: RegisterVerifiableCredential = {
|
||||||
@@ -682,7 +634,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error With Server",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
@@ -707,30 +659,36 @@ 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 {
|
||||||
console.error(
|
console.error("Bad response setting visibility: ", resp.data);
|
||||||
"Got some bad server response when setting visibility: ",
|
if (resp.data.error?.message) {
|
||||||
resp,
|
this.$notify(
|
||||||
);
|
{
|
||||||
const message =
|
group: "alert",
|
||||||
resp.data.error?.message || "Bad server response of " + resp.status;
|
type: "danger",
|
||||||
this.$notify(
|
title: "Error With Server",
|
||||||
{
|
text: resp.data.error?.message,
|
||||||
group: "alert",
|
},
|
||||||
type: "danger",
|
-1,
|
||||||
title: "Server Error",
|
);
|
||||||
text: message,
|
} else {
|
||||||
},
|
this.$notify(
|
||||||
-1,
|
{
|
||||||
);
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Bad server response of " + resp.status,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Got some server error when setting visibility:", err);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error With Server",
|
||||||
text: "Check connectivity and try again.",
|
text: err as string,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -755,7 +713,7 @@ export default class ContactsView extends Vue {
|
|||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "toast",
|
||||||
title: "Refreshed",
|
title: "Refreshed",
|
||||||
text:
|
text:
|
||||||
this.nameForContact(contact, true) +
|
this.nameForContact(contact, true) +
|
||||||
@@ -763,29 +721,38 @@ export default class ContactsView extends Vue {
|
|||||||
(visibility ? "" : "not ") +
|
(visibility ? "" : "not ") +
|
||||||
"see your activity.",
|
"see your activity.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log("Got bad server response when checking visibility: ", resp);
|
if (resp.data.error?.message) {
|
||||||
const message = resp.data.error?.message || "Got bad server response.";
|
this.$notify(
|
||||||
this.$notify(
|
{
|
||||||
{
|
group: "alert",
|
||||||
group: "alert",
|
type: "danger",
|
||||||
type: "danger",
|
title: "Error With Server",
|
||||||
title: "Server Error",
|
text: resp.data.error?.message,
|
||||||
text: message,
|
},
|
||||||
},
|
-1,
|
||||||
-1,
|
);
|
||||||
);
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Bad server response of " + resp.status,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Caught error from server request to check visibility:", err);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error With Server",
|
||||||
text: "Check connectivity and try again.",
|
text: err as string,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -985,7 +952,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error With Server",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
@@ -994,18 +961,6 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onClickCancelName() {
|
|
||||||
this.contactEdit = null;
|
|
||||||
this.contactNewName = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private async onClickSaveName(contact: Contact, newName: string) {
|
|
||||||
contact.name = newName;
|
|
||||||
return db.contacts
|
|
||||||
.update(contact.did, { name: newName })
|
|
||||||
.then(() => (this.contactEdit = null));
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleShowGiveTotals() {
|
public toggleShowGiveTotals() {
|
||||||
if (this.showGiveTotals) {
|
if (this.showGiveTotals) {
|
||||||
this.showGiveTotals = false;
|
this.showGiveTotals = false;
|
||||||
@@ -1030,36 +985,14 @@ export default class ContactsView extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dialog-overlay {
|
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Tooltip, generated on "title" attributes on "fa" icons
|
|
||||||
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
|
||||||
*/
|
|
||||||
/* Tooltip container */
|
/* Tooltip container */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tooltip text */
|
/* Tooltip text */
|
||||||
.tooltip .tooltiptext {
|
.tooltip .tooltiptext {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
searchAll();
|
searchAll();
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Anywhere
|
Remote
|
||||||
<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"
|
||||||
>{{ remoteCount }}</span
|
>{{ remoteCount }}</span
|
||||||
@@ -67,14 +67,46 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLocalActive">
|
<div v-if="isLocalActive">
|
||||||
<div>
|
<div v-if="!isChoosingSearchBox">
|
||||||
<button
|
<button
|
||||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
@click="$router.push({ name: 'search-area' })"
|
@click="isChoosingSearchBox = true"
|
||||||
>
|
>
|
||||||
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Animation -->
|
<!-- Loading Animation -->
|
||||||
@@ -118,11 +150,50 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</InfiniteScroll>
|
</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 { LeafletMouseEvent } from "leaflet";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import {
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LRectangle,
|
||||||
|
LTileLayer,
|
||||||
|
} from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
@@ -133,6 +204,10 @@ import QuickNav from "@/components/QuickNav.vue";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
|
||||||
|
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||||
|
const WORLD_ZOOM = 2;
|
||||||
|
const DEFAULT_ZOOM = 2;
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -142,9 +217,13 @@ interface Notification {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
LRectangle,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LTileLayer,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {
|
export default class DiscoverView extends Vue {
|
||||||
@@ -159,7 +238,13 @@ export default class DiscoverView extends Vue {
|
|||||||
isChoosingSearchBox = false;
|
isChoosingSearchBox = false;
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
isRemoteActive = false;
|
isRemoteActive = false;
|
||||||
|
isNewMarkerSet = false;
|
||||||
|
localCenterLat = 0;
|
||||||
|
localCenterLong = 0;
|
||||||
|
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
localCount = 0;
|
localCount = 0;
|
||||||
|
localZoom = DEFAULT_ZOOM;
|
||||||
remoteCount = 0;
|
remoteCount = 0;
|
||||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
@@ -173,6 +258,7 @@ export default class DiscoverView extends Vue {
|
|||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
|
this.resetLatLong();
|
||||||
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
@@ -376,6 +462,128 @@ export default class DiscoverView extends Vue {
|
|||||||
this.$router.push(route);
|
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() {
|
public computedLocalTabClassNames() {
|
||||||
return {
|
return {
|
||||||
"inline-block": true,
|
"inline-block": true,
|
||||||
|
|||||||
@@ -41,15 +41,14 @@
|
|||||||
You need someone to register you -- usually the person who told you
|
You need someone to register you -- usually the person who told you
|
||||||
about this app, on the Contacts
|
about this app, on the Contacts
|
||||||
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
|
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
|
||||||
you can select any contact on the home page (or "anonymous") and record
|
and after you have contacts, you can select any contact on the home page
|
||||||
your appreciation for... whatever. The main goal is to record what
|
and record your appreciation for... whatever. That is a claim recorded
|
||||||
people have given you, to grow gifting economies. Each claim is recorded
|
|
||||||
on a custom ledger. The day after being registered, you'll be able to
|
on a custom ledger. The day after being registered, you'll be able to
|
||||||
able to register others; later, you can create projects, too.
|
register others; later, you can create projects, too.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Note that there are limits to how many others each person can register,
|
Note that there are limits to how many each person can register, so you
|
||||||
so you may have to wait.
|
may have to wait.
|
||||||
</p>
|
</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>
|
||||||
@@ -131,9 +130,7 @@
|
|||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
||||||
<p>
|
<p>
|
||||||
Before doing this, note that it is an advanced feature that affects
|
Go
|
||||||
functionality (eg. the words "Alt ID" next to results, backup features)
|
|
||||||
so beware if you think that may cause confusion. You can
|
|
||||||
<router-link to="start" class="text-blue-500">
|
<router-link to="start" class="text-blue-500">
|
||||||
create another identity here.
|
create another identity here.
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -6,82 +6,168 @@
|
|||||||
Time Safari
|
Time Safari
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- show the actions for recognizing a give -->
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div v-if="!activeDid">
|
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
|
||||||
To record others' giving,
|
|
||||||
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
|
||||||
create your identifier.</router-link
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="!isRegistered">
|
<button
|
||||||
To record others' giving, someone must register your account, so show
|
@click="
|
||||||
them
|
this.$notify(
|
||||||
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
|
{
|
||||||
your identity info</router-link
|
group: 'alert',
|
||||||
>
|
type: 'toast',
|
||||||
and then
|
text: 'I\'m a toast. Don\'t mind me.',
|
||||||
<router-link :to="{ name: 'account' }" class="text-blue-500">
|
},
|
||||||
check your limits.</router-link
|
5000,
|
||||||
>
|
)
|
||||||
</div>
|
"
|
||||||
|
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Toast (self-dismiss)
|
||||||
|
</button>
|
||||||
|
|
||||||
<div v-else>
|
<button
|
||||||
<!-- activeDid && isRegistered -->
|
@click="
|
||||||
<h2 class="text-xl font-bold">Record a Gift</h2>
|
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>
|
||||||
|
|
||||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
<button
|
||||||
<li @click="openDialog()">
|
@click="
|
||||||
<EntityIcon
|
this.$notify(
|
||||||
:entityId="null"
|
{
|
||||||
:iconSize="64"
|
group: 'alert',
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
type: 'success',
|
||||||
></EntityIcon>
|
title: 'Success Alert',
|
||||||
<h3
|
text: 'Congratulations!',
|
||||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
},
|
||||||
>
|
-1,
|
||||||
Anonymous/Unnamed
|
)
|
||||||
</h3>
|
"
|
||||||
</li>
|
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
<li
|
>
|
||||||
v-for="contact in allContacts"
|
Success
|
||||||
:key="contact.did"
|
</button>
|
||||||
@click="openDialog(contact)"
|
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Notification Permission
|
||||||
|
</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="Anonymous"
|
||||||
|
: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"
|
||||||
>
|
>
|
||||||
<EntityIcon
|
Anonymous
|
||||||
:entityId="contact.did"
|
</h3>
|
||||||
:iconSize="64"
|
</li>
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
<li
|
||||||
></EntityIcon>
|
v-for="contact in allContacts"
|
||||||
<h3
|
:key="contact.did"
|
||||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
@click="openDialog(contact)"
|
||||||
>
|
|
||||||
{{ 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…
|
<EntityIcon
|
||||||
</router-link>
|
: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>
|
||||||
|
|
||||||
<!-- If there are no contacts, show this instead: -->
|
<!-- 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) -->
|
||||||
<div
|
<router-link
|
||||||
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 > 7"
|
||||||
v-if="allContacts.length === 0"
|
:to="{ name: 'contact-gives' }"
|
||||||
>
|
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||||
(No contacts to show.)
|
>
|
||||||
</div>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
|
<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">
|
<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>
|
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
||||||
@@ -117,18 +203,19 @@
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
didInfo,
|
didInfo,
|
||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
|
GiverOutputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -152,7 +239,6 @@ export default class HomeView extends Vue {
|
|||||||
feedPreviousOldestId?: string;
|
feedPreviousOldestId?: string;
|
||||||
feedLastViewedId?: string;
|
feedLastViewedId?: string;
|
||||||
isHiddenSpinner = true;
|
isHiddenSpinner = true;
|
||||||
isRegistered = false;
|
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
@@ -162,10 +248,10 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const account = (await accountsDB.accounts
|
const account = await accountsDB.accounts
|
||||||
.where("did")
|
.where("did")
|
||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first()) as Account;
|
.first();
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@@ -192,12 +278,11 @@ export default class HomeView extends Vue {
|
|||||||
this.allMyDids = allAccounts.map((acc) => acc.did);
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
|
||||||
this.updateAllFeed();
|
this.updateAllFeed();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -223,9 +308,7 @@ export default class HomeView extends Vue {
|
|||||||
if (this.activeDid) {
|
if (this.activeDid) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const allAccounts = await accountsDB.accounts.toArray();
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
const account = allAccounts.find(
|
const account = allAccounts.find((acc) => acc.did === this.activeDid);
|
||||||
(acc) => acc.did === this.activeDid,
|
|
||||||
) as Account;
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@@ -302,7 +385,6 @@ export default class HomeView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
giveDescription(giveRecord: GiveServerRecord) {
|
giveDescription(giveRecord: GiveServerRecord) {
|
||||||
// similar code is in endorser-mobile utility.ts
|
|
||||||
// claim.claim happen for some claims wrapped in a Verifiable Credential
|
// claim.claim happen for some claims wrapped in a Verifiable Credential
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
|
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
|
||||||
@@ -315,18 +397,9 @@ export default class HomeView extends Vue {
|
|||||||
this.allMyDids,
|
this.allMyDids,
|
||||||
this.allContacts,
|
this.allContacts,
|
||||||
);
|
);
|
||||||
let gaveAmount = claim.object?.amountOfThisGood
|
const gaveAmount = claim.object?.amountOfThisGood
|
||||||
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||||
: "";
|
: claim.description || "something unknown";
|
||||||
if (claim.description) {
|
|
||||||
if (gaveAmount) {
|
|
||||||
gaveAmount = gaveAmount + ", and also: ";
|
|
||||||
}
|
|
||||||
gaveAmount = gaveAmount + claim.description;
|
|
||||||
}
|
|
||||||
if (!gaveAmount) {
|
|
||||||
gaveAmount = "something not described";
|
|
||||||
}
|
|
||||||
// recipient.did is for legacy data, before March 2023
|
// recipient.did is for legacy data, before March 2023
|
||||||
const gaveRecipientId =
|
const gaveRecipientId =
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -340,7 +413,7 @@ export default class HomeView extends Vue {
|
|||||||
this.allContacts,
|
this.allContacts,
|
||||||
)
|
)
|
||||||
: "";
|
: "";
|
||||||
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
displayAmount(code: string, amt: number) {
|
displayAmount(code: string, amt: number) {
|
||||||
@@ -354,5 +427,124 @@ export default class HomeView extends Vue {
|
|||||||
openDialog(giver: GiverInputInfo) {
|
openDialog(giver: GiverInputInfo) {
|
||||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
(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>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
||||||
<span class="overflow-hidden">
|
<span class="overflow-hidden">
|
||||||
<h2 class="text-xl font-semibold mb-0">
|
<h2 class="text-xl font-semibold mb-0">
|
||||||
{{ givenName }}
|
{{ firstName }} {{ lastName }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-sm text-slate-500 truncate">
|
<div class="text-sm text-slate-500 truncate">
|
||||||
<b>ID:</b> <code>{{ activeDid }}</code>
|
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||||
@@ -49,9 +49,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<!-- id used by puppeteer test script -->
|
|
||||||
<router-link
|
<router-link
|
||||||
id="start-link"
|
|
||||||
:to="{ name: 'start' }"
|
: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"
|
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
@@ -71,7 +69,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { AccountsSchema } from "@/db/tables/accounts";
|
import { AccountsSchema } from "@/db/tables/accounts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
@@ -90,7 +88,8 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
public activeDid = "";
|
public activeDid = "";
|
||||||
public apiServer = "";
|
public apiServer = "";
|
||||||
public apiServerInput = "";
|
public apiServerInput = "";
|
||||||
public givenName = "";
|
public firstName = "";
|
||||||
|
public lastName = "";
|
||||||
public otherIdentities: Array<{ did: string }> = [];
|
public otherIdentities: Array<{ did: string }> = [];
|
||||||
public showContactGives = false;
|
public showContactGives = false;
|
||||||
|
|
||||||
@@ -100,20 +99,19 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
.where("did")
|
.where("did")
|
||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first();
|
.first();
|
||||||
const identity = JSON.parse((account?.identity as string) || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
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.givenName =
|
this.firstName = settings?.firstName || "No";
|
||||||
(settings?.firstName || "") +
|
this.lastName = settings?.lastName || "Name";
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
|
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
@@ -151,7 +149,7 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
did = undefined;
|
did = undefined;
|
||||||
}
|
}
|
||||||
await db.open();
|
await db.open();
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
activeDid: did,
|
activeDid: did,
|
||||||
});
|
});
|
||||||
this.activeDid = did || "";
|
this.activeDid = did || "";
|
||||||
|
|||||||
@@ -17,9 +17,7 @@
|
|||||||
<p class="text-center text-xl mb-4 font-light">
|
<p class="text-center text-xl mb-4 font-light">
|
||||||
Enter your seed phrase below to import your identity on this device.
|
Enter your seed phrase below to import your identity on this device.
|
||||||
</p>
|
</p>
|
||||||
<!-- id used by puppeteer test script -->
|
|
||||||
<input
|
<input
|
||||||
id="seed-input"
|
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Seed Phrase"
|
placeholder="Seed Phrase"
|
||||||
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"
|
||||||
|
|||||||
@@ -10,15 +10,21 @@
|
|||||||
>
|
>
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
Edit Identity
|
[New/Edit] Identity
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="First Name"
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
v-model="givenName"
|
v-model="firstName"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Last Name"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
v-model="lastName"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
@@ -44,30 +50,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { db } from "@/db/index";
|
import { db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class NewEditAccountView extends Vue {
|
export default class NewEditAccountView extends Vue {
|
||||||
givenName = "";
|
firstName =
|
||||||
|
localStorage.getItem("firstName") === null
|
||||||
|
? "--"
|
||||||
|
: localStorage.getItem("firstName");
|
||||||
|
lastName =
|
||||||
|
localStorage.getItem("lastName") === null
|
||||||
|
? "--"
|
||||||
|
: localStorage.getItem("lastName");
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
// '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)) as Settings;
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.givenName =
|
this.firstName = settings?.firstName || "";
|
||||||
(settings?.firstName || "") +
|
this.lastName = settings?.lastName || "";
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickSaveChanges() {
|
onClickSaveChanges() {
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
firstName: this.givenName,
|
firstName: this.firstName,
|
||||||
lastName: "", // deprecated, pre v 0.1.3
|
lastName: this.lastName,
|
||||||
});
|
});
|
||||||
localStorage.setItem("firstName", this.givenName as string);
|
localStorage.setItem("firstName", this.firstName as string);
|
||||||
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
|
localStorage.setItem("lastName", this.lastName as string);
|
||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Projects"></QuickNav>
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
@@ -50,11 +49,6 @@
|
|||||||
<label for="includeLocation">Include Location</label>
|
<label for="includeLocation">Include Location</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
||||||
<div class="px-2 py-2">
|
|
||||||
For your security, we recommend you choose a location nearby but not
|
|
||||||
exactly at the place.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<l-map
|
<l-map
|
||||||
ref="map"
|
ref="map"
|
||||||
v-model:zoom="zoom"
|
v-model:zoom="zoom"
|
||||||
@@ -113,7 +107,6 @@ import * as didJwt from "did-jwt";
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
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";
|
||||||
@@ -129,7 +122,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { LMap, LMarker, LTileLayer, QuickNav },
|
components: { LMap, LMarker, LTileLayer },
|
||||||
})
|
})
|
||||||
export default class NewEditProjectView extends Vue {
|
export default class NewEditProjectView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
export default class NewIdentifierView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
|||||||
@@ -95,14 +95,14 @@
|
|||||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
<li @click="openDialog()">
|
<li @click="openDialog()">
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:entityId="null"
|
:entityId="Anonymous"
|
||||||
:iconSize="64"
|
:iconSize="64"
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
></EntityIcon>
|
></EntityIcon>
|
||||||
<h3
|
<h3
|
||||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
>
|
>
|
||||||
Anonymous/Unnamed
|
Anonymous
|
||||||
</h3>
|
</h3>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
|
|
||||||
<!-- 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) -->
|
<!-- 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
|
<router-link
|
||||||
v-if="allContacts.length >= 7"
|
v-if="allContacts.length > 7"
|
||||||
:to="{ name: 'contact-gives' }"
|
:to="{ name: 'contact-gives' }"
|
||||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
@@ -137,12 +137,10 @@
|
|||||||
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
<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">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
Given To This Project
|
Given to this Project
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
|
<ul class="text-sm border-t border-slate-300">
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
|
||||||
<li
|
<li
|
||||||
v-for="give in givesToThis"
|
v-for="give in givesToThis"
|
||||||
:key="give.id"
|
:key="give.id"
|
||||||
@@ -166,33 +164,31 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="fulfilledByThis" 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">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
Contributions By This Project
|
…and from this Project
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
|
||||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
|
||||||
class="text-blue-500"
|
|
||||||
>
|
|
||||||
{{ fulfilledByThis.name }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<ul class="text-sm border-t border-slate-300">
|
||||||
v-if="fulfillersToThis.length > 0"
|
<li
|
||||||
class="bg-slate-100 px-4 py-3 rounded-md"
|
v-for="give in givesByThis"
|
||||||
>
|
:key="give.id"
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
class="py-1.5 border-b border-slate-300"
|
||||||
Contributions To This Project
|
>
|
||||||
</h3>
|
<div class="flex justify-between gap-4">
|
||||||
<ul>
|
<span
|
||||||
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
<button
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
@click="onClickLoadProject(plan.handleId)"
|
</span>
|
||||||
class="text-blue-500"
|
<span v-if="give.amount"
|
||||||
>
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
{{ plan.name }}
|
{{ give.amount }}
|
||||||
</button>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,8 +196,8 @@
|
|||||||
|
|
||||||
<GiftedDialog
|
<GiftedDialog
|
||||||
ref="customDialog"
|
ref="customDialog"
|
||||||
|
@dialog-result="handleDialogResult"
|
||||||
message="Received from"
|
message="Received from"
|
||||||
:projectId="this.projectId"
|
|
||||||
>
|
>
|
||||||
</GiftedDialog>
|
</GiftedDialog>
|
||||||
</section>
|
</section>
|
||||||
@@ -216,17 +212,18 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
didInfo,
|
didInfo,
|
||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
|
GiverOutputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
PlanServerRecord,
|
ResultWithType,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -247,9 +244,8 @@ export default class ProjectViewView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
description = "";
|
description = "";
|
||||||
expanded = false;
|
expanded = false;
|
||||||
fulfilledByThis: PlanServerRecord | null = null;
|
|
||||||
fulfillersToThis: Array<PlanServerRecord> = [];
|
|
||||||
givesToThis: Array<GiveServerRecord> = [];
|
givesToThis: Array<GiveServerRecord> = [];
|
||||||
|
givesByThis: Array<GiveServerRecord> = [];
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
name = "";
|
name = "";
|
||||||
@@ -261,7 +257,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
@@ -270,22 +266,17 @@ export default class ProjectViewView extends Vue {
|
|||||||
const accounts = accountsDB.accounts;
|
const accounts = accountsDB.accounts;
|
||||||
const accountsArr = await accounts?.toArray();
|
const accountsArr = await accounts?.toArray();
|
||||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
const account = accountsArr?.find((acc) => acc.did === this.activeDid);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
this.LoadProject(identity);
|
||||||
const pathParam = window.location.pathname.substring("/project/".length);
|
|
||||||
if (pathParam) {
|
|
||||||
this.projectId = decodeURIComponent(pathParam);
|
|
||||||
}
|
|
||||||
this.LoadProject(this.projectId, identity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const account = (await accountsDB.accounts
|
const account = await accountsDB.accounts
|
||||||
.where("did")
|
.where("did")
|
||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first()) as Account;
|
.first();
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@@ -331,11 +322,11 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async LoadProject(projectId: string, identity: IIdentifier) {
|
async LoadProject(identity: IIdentifier) {
|
||||||
this.projectId = projectId;
|
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
|
this.apiServer +
|
||||||
|
"/api/claim/byHandle/" +
|
||||||
|
encodeURIComponent(this.projectId);
|
||||||
const headers: RawAxiosRequestHeaders = {
|
const headers: RawAxiosRequestHeaders = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
@@ -400,7 +391,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
const givesInUrl =
|
const givesInUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/givesForPlans?planIds=" +
|
"/api/v2/report/givesForPlans?planIds=" +
|
||||||
encodeURIComponent(JSON.stringify([projectId]));
|
encodeURIComponent(JSON.stringify([this.projectId]));
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(givesInUrl, { headers });
|
const resp = await this.axios.get(givesInUrl, { headers });
|
||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200 && resp.data.data) {
|
||||||
@@ -433,21 +424,21 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fulfilledByUrl =
|
const givesOutUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
"/api/v2/report/givesProvidedBy?providerId=" +
|
||||||
encodeURIComponent(projectId);
|
encodeURIComponent(this.projectId);
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(fulfilledByUrl, { headers });
|
const resp = await this.axios.get(givesOutUrl, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200 && resp.data.data) {
|
||||||
this.fulfilledByThis = resp.data.data;
|
this.givesByThis = resp.data.data;
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Failed to retrieve plans fulfilled by this project.",
|
text: "Failed to retrieve gives by this project.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -459,64 +450,20 @@ export default class ProjectViewView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Something went wrong retrieving plans fulfilled by this project.",
|
text: "Something went wrong retrieving gives by project.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Error retrieving plans fulfilled by this project:",
|
"Error retrieving gives by this project:",
|
||||||
serverError.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fulfillersToUrl =
|
|
||||||
this.apiServer +
|
|
||||||
"/api/v2/report/planFulfillersToPlan?planHandleId=" +
|
|
||||||
encodeURIComponent(projectId);
|
|
||||||
try {
|
|
||||||
const resp = await this.axios.get(fulfillersToUrl, { headers });
|
|
||||||
if (resp.status === 200) {
|
|
||||||
this.fulfillersToThis = resp.data.data;
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Failed to retrieve plan fulfillers 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 plan fulfillers to this project.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Error retrieving plan fulfillers to this project:",
|
|
||||||
serverError.message,
|
serverError.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
openDialog(contact: GiverInputInfo) {
|
||||||
* Handle clicking on a project entry found in the list
|
const dialog: GiftedDialog = this.$refs.customDialog as GiftedDialog;
|
||||||
* @param id of the project
|
dialog.open(contact);
|
||||||
**/
|
|
||||||
async onClickLoadProject(projectId: string) {
|
|
||||||
localStorage.setItem("projectId", projectId);
|
|
||||||
const route = {
|
|
||||||
path: "/project/" + encodeURIComponent(projectId),
|
|
||||||
};
|
|
||||||
this.$router.push(route);
|
|
||||||
this.LoadProject(projectId, await this.getIdentity(this.activeDid));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenStreetMapUrl() {
|
getOpenStreetMapUrl() {
|
||||||
@@ -533,8 +480,96 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
openDialog(contact: GiverInputInfo) {
|
handleDialogResult(result: GiverOutputInfo) {
|
||||||
(this.$refs.customDialog as GiftedDialog).open(contact);
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ export default class ProjectsView extends Vue {
|
|||||||
projects: ProjectData[] = [];
|
projects: ProjectData[] = [];
|
||||||
current: IIdentifier;
|
current: IIdentifier;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
alertTitle = "";
|
||||||
|
alertMessage = "";
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
|
|||||||
@@ -1,281 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<section id="Content" class="p-6 pb-24">
|
|
||||||
<!-- Breadcrumb -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<!-- Back -->
|
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Area for Nearby Search
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-2 py-4">
|
|
||||||
This location is only stored on your device. It is used to show you more
|
|
||||||
appropriate projects but is not stored on any servers.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
|
||||||
Click to Choose a Location 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="searchBox"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="resetLatLong"
|
|
||||||
>
|
|
||||||
Reset Marker
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="isNewMarkerSet = false"
|
|
||||||
>
|
|
||||||
Erase Marker
|
|
||||||
</button>
|
|
||||||
<div v-if="isNewMarkerSet">
|
|
||||||
Click on the pin to erase it. Click anywhere else to set a different
|
|
||||||
different corner.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div 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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
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";
|
|
||||||
|
|
||||||
import { db } from "@/db/index";
|
|
||||||
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
||||||
|
|
||||||
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,
|
|
||||||
LMap,
|
|
||||||
LMarker,
|
|
||||||
LTileLayer,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class DiscoverView extends Vue {
|
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
isChoosingSearchBox = false;
|
|
||||||
isNewMarkerSet = false;
|
|
||||||
|
|
||||||
// "local" vars are for the currently selected map box
|
|
||||||
localCenterLat = 0;
|
|
||||||
localCenterLong = 0;
|
|
||||||
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
localZoom = DEFAULT_ZOOM;
|
|
||||||
|
|
||||||
// searchBox reflects what is stored in the database
|
|
||||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
await db.open();
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
|
||||||
this.resetLatLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
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.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Saved",
|
|
||||||
text: "That has been saved in your preferences.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
} 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;
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -8,8 +8,7 @@
|
|||||||
Start Here
|
Start Here
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
<div class="mt-8">
|
||||||
<div id="start-question" 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 have an identity to import?
|
Do you have an identity to import?
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
<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">
|
|
||||||
Test
|
|
||||||
</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. Without a timeout, I\'m stuck.',
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="font-bold uppercase bg-slate-900 text-white px-3 py-2 rounded-md mr-2"
|
|
||||||
>
|
|
||||||
Toast
|
|
||||||
</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>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
|
||||||
export default class Help extends Vue {}
|
|
||||||
</script>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const notifications = require("./safari-notifications.js");
|
|
||||||
|
|
||||||
|
|
||||||
self.addEventListener("push", function (event) {
|
|
||||||
let payload;
|
|
||||||
if (event.data) {
|
|
||||||
payload = JSON.parse(event.data.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = payload ? payload.title : "Custom Title";
|
|
||||||
const options = {
|
|
||||||
body: payload ? payload.body : "Custom body text",
|
|
||||||
icon: payload ? payload.icon : "icon.png",
|
|
||||||
badge: payload ? payload.badge : "badge.png",
|
|
||||||
};
|
|
||||||
|
|
||||||
event.waitUntil(self.registration.showNotification(title, options));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
self.addEventListener("message", function (event) {
|
|
||||||
const data = event.data;
|
|
||||||
|
|
||||||
const result = notifications.getNotificationCount()
|
|
||||||
|
|
||||||
switch (data.command) {
|
|
||||||
case "account":
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log("Unknown command:", data.command);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,5 @@ module.exports = defineConfig({
|
|||||||
iconPaths: {
|
iconPaths: {
|
||||||
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
||||||
},
|
},
|
||||||
workboxOptions: {
|
|
||||||
importScripts: ["additional-scripts.js"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
38
web-push.md
38
web-push.md
@@ -1,6 +1,3 @@
|
|||||||
|
|
||||||
# Overivew of Web Push
|
|
||||||
|
|
||||||
Web Push notifications is a web browser messaging protocol defined by the W3C.
|
Web Push notifications is a web browser messaging protocol defined by the W3C.
|
||||||
|
|
||||||
Discussions of this interesting technology are clouded because of a
|
Discussions of this interesting technology are clouded because of a
|
||||||
@@ -29,8 +26,8 @@ from the SERVICE.
|
|||||||
The SERVICE will provide context and obtain explicit permission before prompting
|
The SERVICE will provide context and obtain explicit permission before prompting
|
||||||
for notification permission:
|
for notification permission:
|
||||||
|
|
||||||
In order to provide this context and explicit permission, a two-step opt-in process
|
In order to provide this context and explict permission a two-step opt-in process
|
||||||
first presents the user with a pre-permission dialog box that explains
|
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
|
what the notifications are for and why they are useful. This may help reduce the
|
||||||
possibility of users clicking "don't allow".
|
possibility of users clicking "don't allow".
|
||||||
|
|
||||||
@@ -91,7 +88,7 @@ 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
|
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.
|
means of communicating between itself and the web page via messages.
|
||||||
|
|
||||||
Note that there is a scope that can specify what network requests it may
|
Note that there is a scope can specify what network requests it may
|
||||||
intercept.
|
intercept.
|
||||||
|
|
||||||
The Vue project already has its own service worker but it is possible to
|
The Vue project already has its own service worker but it is possible to
|
||||||
@@ -361,32 +358,3 @@ unsubscribeFromPush().catch((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.
|
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