forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ccfb283b4 | |||
| 552fce3281 | |||
| 12de3dec4f | |||
| b171e1ae13 | |||
| dc54006fca | |||
| 9b4db018f5 | |||
| 519f320a2e | |||
|
|
f1b3094026 | ||
|
|
e5ad87f4d5 | ||
|
|
7de6171911 | ||
|
|
bb6bacac97 | ||
|
|
40fc6a29a4 | ||
|
|
9ec19fa4ee | ||
| 28b20f86ea | |||
| 502109de4b | |||
| 97274a701d | |||
| 81a6d73f2f | |||
| 5804f692b7 | |||
| 257aa8d49e | |||
| 34806b514b | |||
| 0024238ca8 | |||
| 0af05b4b0d | |||
| b9d59eb642 | |||
| 0c05505c46 | |||
| 98c093f655 | |||
| 88112e0629 | |||
|
|
6ab92a83bd | ||
|
|
bfc52151c0 | ||
|
|
868b5413de | ||
|
|
50005a0dc3 | ||
|
|
9247b6ed1f | ||
|
|
75f26ccf2d | ||
|
|
bfd2498906 | ||
|
|
4933017e9c | ||
|
|
18c23451bb | ||
|
|
304985f88d | ||
|
|
9a41aff8f0 | ||
|
|
e19cd980b4 | ||
| 6d1756b4a5 | |||
| ac4c92d8e8 | |||
| 937a3cb6c6 | |||
| 194f741984 | |||
| b31c0d975c | |||
| bf6830a1a8 | |||
|
|
fe09f5180d | ||
|
|
5addc3c206 | ||
|
|
69f2f3cfd2 | ||
|
|
4de66b1609 | ||
|
|
4b87692231 | ||
|
|
503bb1bd93 | ||
|
|
9fa3b8be0b | ||
| 3b1a9b9c5b | |||
| 7f48149d6f | |||
| c5b4921583 | |||
| b28689ad06 | |||
| 0444b5be32 | |||
| be348461f1 | |||
| 6e2c596030 | |||
|
|
c502869c5f | ||
|
|
b7aacd63e6 | ||
|
|
5bc0e27b30 | ||
|
|
a4fe94f081 | ||
|
|
8de95566df | ||
|
|
97569697f6 | ||
|
|
b9ed9d748b | ||
|
|
790d44db81 | ||
| e2bf469dc1 | |||
| 592ffacebc | |||
| b706e65598 | |||
|
|
6e3066ae92 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,13 +1,16 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
|
signature.bin
|
||||||
|
*.pem
|
||||||
|
verified.txt
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
# Log files
|
# Log filesopenssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ npm run serve
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
### Compiles and minifies for production
|
||||||
|
|
||||||
|
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
jq
|
||||||
|
|
||||||
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
||||||
|
|
||||||
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
||||||
|
|||||||
25
openssl_signing_console.sh
Executable file
25
openssl_signing_console.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
||||||
|
openssl ec -in private.pem -pubout -out public.pem
|
||||||
|
|
||||||
|
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
||||||
|
|
||||||
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
||||||
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
||||||
|
|
||||||
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
|
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
|
||||||
|
|
||||||
|
# Read binary signature from file and encode it to Base64 URL-Safe format
|
||||||
|
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
|
# Construct the JWT
|
||||||
|
jwt="$signing_input.$signature_b64"
|
||||||
|
|
||||||
|
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
|
||||||
|
|
||||||
|
|
||||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -33,6 +33,7 @@
|
|||||||
"ethereum-cryptography": "^2.0.0",
|
"ethereum-cryptography": "^2.0.0",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"ethr-did-resolver": "^8.0.0",
|
"ethr-did-resolver": "^8.0.0",
|
||||||
|
"jdenticon": "^3.2.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"localstorage-slim": "^2.4.0",
|
"localstorage-slim": "^2.4.0",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"@types/three": "^0.152.1",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-pwa": "~5.0.8",
|
"@vue/cli-plugin-pwa": "~5.0.8",
|
||||||
@@ -73,6 +75,7 @@
|
|||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
@@ -9638,6 +9641,24 @@
|
|||||||
"uint8arrays": "^3.0.0"
|
"uint8arrays": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue-leaflet/vue-leaflet": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.10.1.tgz",
|
||||||
|
"integrity": "sha512-RNEDk8TbnwrJl8ujdbKgZRFygLCxd0aBcWLQ05q/pGv4+d0jamE3KXQgQBqGAteE1mbQsk3xoNcqqUgaIGfWVg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/leaflet": "^1.5.7",
|
||||||
|
"leaflet": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/leaflet": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz",
|
||||||
@@ -12049,6 +12070,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.0.0.tgz",
|
||||||
"integrity": "sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w=="
|
"integrity": "sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/canvas-renderer": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvas-renderer/-/canvas-renderer-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-RrBgVL5qCEDIXpJ6NrzyRNoTnXxYarqm/cS/W6ERhUJts5UQtt/XPEosGN3rqUkZ4fjBArlnCbsISJ+KCFnIAg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/case-sensitive-paths-webpack-plugin": {
|
"node_modules/case-sensitive-paths-webpack-plugin": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz",
|
||||||
@@ -17798,6 +17827,20 @@
|
|||||||
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
|
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/jdenticon": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jdenticon/-/jdenticon-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-z6Iq3fTODUMSOiR2nNYrqigS6Y0GvdXfyQWrUby7htDHvX7GNEwaWR4hcaL+FmhEgBe08Xkup/BKxXQhDJByPA==",
|
||||||
|
"dependencies": {
|
||||||
|
"canvas-renderer": "~2.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"jdenticon": "bin/jdenticon.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-environment-node": {
|
"node_modules/jest-environment-node": {
|
||||||
"version": "29.5.0",
|
"version": "29.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz",
|
||||||
@@ -19044,6 +19087,12 @@
|
|||||||
"launch-editor": "^2.6.0"
|
"launch-editor": "^2.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"ethereum-cryptography": "^2.0.0",
|
"ethereum-cryptography": "^2.0.0",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"ethr-did-resolver": "^8.0.0",
|
"ethr-did-resolver": "^8.0.0",
|
||||||
|
"jdenticon": "^3.2.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"localstorage-slim": "^2.4.0",
|
"localstorage-slim": "^2.4.0",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"@types/three": "^0.152.1",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-pwa": "~5.0.8",
|
"@vue/cli-plugin-pwa": "~5.0.8",
|
||||||
@@ -73,6 +75,7 @@
|
|||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
|
|||||||
@@ -1,41 +1,57 @@
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
- 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
|
||||||
|
- 01 add a location for a project via map pin :
|
||||||
|
- add with a "location" field containing this: { "geo":{ "@type":"GeoCoordinates", "latitude":40.883944, "longitude":-111.884787 } }
|
||||||
|
- 40 notifications :
|
||||||
|
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||||
|
|
||||||
- 01 add a location for a project via map pin
|
- 01 add a location for a project via map pin
|
||||||
- 04 search by a bounding box for local projects (see API by clicking on "Nearby")
|
- 04 search by a bounding box for local projects (see API by clicking on "Nearby")
|
||||||
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:jose
|
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
||||||
- 02 Fix images on projectview - allow choice of image from a pallete of images or a url image.
|
- 02 Fix images on projectview - allow choice of image from a pallete of images or a url image (discovery page display also)
|
||||||
|
- SEE: https://github.com/dmester/jdenticon assignee:jose
|
||||||
|
|
||||||
- 08 Scan QR code to import into contacts.
|
- 08 Scan QR code to import into contacts assignee:matthew
|
||||||
|
- SEE: https://github.com/gruhn/vue-qrcode-reader
|
||||||
|
|
||||||
- 40 notifications :
|
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
|
||||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data
|
|
||||||
|
|
||||||
- refactor 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
|
||||||
- .5 Alerts show at the top and can be missed if you've scrolled down on the page, eg. account data download
|
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui
|
||||||
- .2 Make alerts at the top more visible (because they're currently a similar color and sometimes aren't seen)
|
|
||||||
|
|
||||||
- Show pop-up or some message confirming that settings & contacts download has been initiated/finished
|
|
||||||
|
|
||||||
- Ensure each action sent to the server has a confirmation - eg registration
|
|
||||||
|
|
||||||
- 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")
|
||||||
- 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
|
- 01 quick action - send action, maybe choose via canvas tool
|
||||||
|
- SEE: https://github.com/konvajs/vue-konva
|
||||||
|
|
||||||
- 24 Move to Vite
|
- 24 Move to Vite assignee:matthew
|
||||||
|
|
||||||
|
- .5 include the hash of the latest commit, and maybe a version
|
||||||
- .5 add link to further project / people when a project pays ahead
|
- .5 add link to further project / people when a project pays ahead
|
||||||
- .5 add project ID to the URL, 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 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 there are three dots at the top of ProjectViewView that refreshes the page but doesn't do anything else
|
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist assignee-group:ui
|
||||||
- 01 fix images on project page, on discovery page
|
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui
|
||||||
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist
|
|
||||||
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?)
|
|
||||||
- .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
|
||||||
|
- .2 move 'switch identity' to the advanced section assignee-group:ui
|
||||||
|
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
||||||
|
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||||
|
|
||||||
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
||||||
|
|
||||||
|
- 04 allow user to download claims, mine + ones I can see about me from others
|
||||||
|
- .5 change the derivation path, and regenerate test IDs
|
||||||
|
- 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path)
|
||||||
|
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages assignee-group:ui
|
||||||
|
- .5 customize favicon assignee-group:ui
|
||||||
|
- .5 Do we want to combine first name & last name?
|
||||||
|
- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui
|
||||||
|
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
||||||
|
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
- .2 show error to user when adding a duplicate contact
|
- .2 show error to user when adding a duplicate contact
|
||||||
@@ -48,17 +64,10 @@ tasks:
|
|||||||
- 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")
|
||||||
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
||||||
|
|
||||||
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages
|
|
||||||
- .5 customize favicon
|
|
||||||
- 04 allow user to download claims, mine + ones I can see about me from others
|
|
||||||
- Do we want to combine first name & last name?
|
|
||||||
- Show a warning if both giver and recipient are the same (but still allow?)
|
|
||||||
|
|
||||||
- Release Minimum Viable Product :
|
- Release Minimum Viable Product :
|
||||||
- 08 thorough testing for errors & edge cases
|
- 08 thorough testing for errors & edge cases
|
||||||
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
||||||
- Add disclaimers.
|
- Add disclaimers.
|
||||||
- Rename DB to TimeSafari.
|
|
||||||
- Switch default server to the public server.
|
- Switch default server to the public server.
|
||||||
- Deploy to a server.
|
- Deploy to a server.
|
||||||
- Ensure public server has limits that work for group adoption.
|
- Ensure public server has limits that work for group adoption.
|
||||||
|
|||||||
58
src/App.vue
58
src/App.vue
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|
||||||
|
<!-- https://github.com/emmanuelsw/notiwind -->
|
||||||
<NotificationGroup group="alert">
|
<NotificationGroup group="alert">
|
||||||
<div
|
<div
|
||||||
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
|
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
|
||||||
@@ -127,6 +128,63 @@
|
|||||||
</Notification>
|
</Notification>
|
||||||
</div>
|
</div>
|
||||||
</NotificationGroup>
|
</NotificationGroup>
|
||||||
|
|
||||||
|
<NotificationGroup group="modal">
|
||||||
|
<div class="fixed z-[100] top-0 inset-x-0 w-full">
|
||||||
|
<Notification
|
||||||
|
v-slot="{ notifications, close }"
|
||||||
|
enter="transform ease-out duration-300 transition"
|
||||||
|
enter-from="translate-y-2 opacity-0 sm:translate-y-4"
|
||||||
|
enter-to="translate-y-0 opacity-100 sm:translate-y-0"
|
||||||
|
leave="transition ease-in duration-500"
|
||||||
|
leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0"
|
||||||
|
move="transition duration-500"
|
||||||
|
move-delay="delay-300"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="notification in notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
class="w-full"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'notification-permission'"
|
||||||
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
|
<p class="text-lg mb-4">
|
||||||
|
Would you like to turn on notifications for this app?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Turn on Notifications
|
||||||
|
</button>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Maybe Later
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Never
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Notification>
|
||||||
|
</div>
|
||||||
|
</NotificationGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
19
src/components/EntityIcon.vue
Normal file
19
src/components/EntityIcon.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div v-html="generateIdenticon()" class="w-fit"></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
import { toSvg } from "jdenticon";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class EntityIcon extends Vue {
|
||||||
|
@Prop entityId = "";
|
||||||
|
@Prop iconSize = "";
|
||||||
|
|
||||||
|
generateIdenticon() {
|
||||||
|
const svgString = toSvg(this.entityId, this.iconSize);
|
||||||
|
return svgString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
export enum AppString {
|
export enum AppString {
|
||||||
APP_NAME = "Kick-Start with Time",
|
APP_NAME = "Kick-Start with Time",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://endorser.ch:3000",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||||
|
|
||||||
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ type NonsensitiveTables = {
|
|||||||
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
|
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
|
||||||
*/
|
*/
|
||||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||||
export const accountsDB = new BaseDexie("KickStartAccounts") as SensitiveDexie;
|
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||||
const SensitiveSchemas = Object.assign({}, AccountsSchema);
|
const SensitiveSchemas = Object.assign({}, AccountsSchema);
|
||||||
|
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||||
BaseDexie & T;
|
BaseDexie & T;
|
||||||
export const db = new BaseDexie("KickStart") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { HDNode } from "@ethersproject/hdnode";
|
|||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import * as u8a from "uint8arrays";
|
import * as u8a from "uint8arrays";
|
||||||
|
|
||||||
|
export const DEFAULT_ROOT_DERIVATION_PATH = "m/76798669'/0'/0'/0'";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
@@ -47,17 +49,17 @@ export const newIdentifier = (
|
|||||||
*/
|
*/
|
||||||
export const deriveAddress = (
|
export const deriveAddress = (
|
||||||
mnemonic: string,
|
mnemonic: string,
|
||||||
|
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
): [string, string, string, string] => {
|
): [string, string, string, string] => {
|
||||||
const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
|
|
||||||
mnemonic = mnemonic.trim().toLowerCase();
|
mnemonic = mnemonic.trim().toLowerCase();
|
||||||
|
|
||||||
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
|
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
|
||||||
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH);
|
const rootNode: HDNode = hdnode.derivePath(derivationPath);
|
||||||
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
|
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
|
||||||
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
|
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
|
||||||
const address = rootNode.address;
|
const address = rootNode.address;
|
||||||
|
|
||||||
return [address, privateHex, publicHex, UPORT_ROOT_DERIVATION_PATH];
|
return [address, privateHex, publicHex, derivationPath];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function didInfo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
* @param identity
|
* @param identity
|
||||||
* @param fromDid may be null
|
* @param fromDid may be null
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import "./assets/styles/tailwind.css";
|
|||||||
|
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||||
import {
|
import {
|
||||||
|
faArrowLeft,
|
||||||
|
faArrowRight,
|
||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
@@ -54,6 +56,8 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
faArrowLeft,
|
||||||
|
faArrowRight,
|
||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "import-account" */ "../views/ImportAccountView.vue"
|
/* webpackChunkName: "import-account" */ "../views/ImportAccountView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/import-derive",
|
||||||
|
name: "import-derive",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "import-derive" */ "../views/ImportDerivedAccountView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/new-edit-account",
|
path: "/new-edit-account",
|
||||||
name: "new-edit-account",
|
name: "new-edit-account",
|
||||||
@@ -184,8 +192,7 @@ const router = createRouter({
|
|||||||
|
|
||||||
const errorHandler = (error, to, from) => {
|
const errorHandler = (error, to, from) => {
|
||||||
// Handle the error here
|
// Handle the error here
|
||||||
console.error(error, to, from);
|
console.error("Caught in top level error handler:", error, to, from);
|
||||||
console.log("XXXXX");
|
|
||||||
|
|
||||||
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,10 +119,24 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'identity-switcher' }"
|
:to="{ name: 'identity-switcher' }"
|
||||||
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
|
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
Switch Identity / No Identity
|
Switch Identity / No Identity
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'modal',
|
||||||
|
type: 'notification-permission',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="block w-full text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
|
||||||
|
>
|
||||||
|
Turn on Notifications
|
||||||
|
</button>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,95 @@
|
|||||||
<template>
|
<template>
|
||||||
<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 -->
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
<!-- Back -->
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'contacts' }"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
|
></router-link>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Given with {{ contact?.name }}
|
Given with {{ contact?.name }}
|
||||||
</h1>
|
</h1>
|
||||||
|
<div class="flex justify-around">
|
||||||
|
<span />
|
||||||
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<div>
|
<table
|
||||||
<div class="border-b border-slate-300 flex">
|
class="table-auto w-full border-t border-slate-300 text-sm sm:text-base text-center"
|
||||||
<div class="w-1/4"></div>
|
>
|
||||||
<div class="w-1/4">from them</div>
|
<thead class="bg-slate-100">
|
||||||
<div class="w-1/4"></div>
|
<tr class="border-b border-slate-300">
|
||||||
<div class="w-1/4">to them</div>
|
<th></th>
|
||||||
</div>
|
<th class="px-1 py-2">From Them</th>
|
||||||
<div
|
<th></th>
|
||||||
class="border-b border-slate-300 flex"
|
<th class="px-1 py-2">To Them</th>
|
||||||
v-for="record in giveRecords"
|
</tr>
|
||||||
:key="record.id"
|
</thead>
|
||||||
>
|
<tbody>
|
||||||
<div class="w-1/4">
|
<tr
|
||||||
{{ new Date(record.issuedAt).toLocaleString() }}
|
v-for="record in giveRecords"
|
||||||
</div>
|
:key="record.id"
|
||||||
<div class="w-1/4">
|
class="border-b border-slate-300"
|
||||||
<span v-if="record.agentDid == contact.did">
|
>
|
||||||
<div class="font-bold">
|
<td class="p-1 text-xs sm:text-sm text-left text-slate-500">
|
||||||
{{ record.amount }} {{ record.unit }}
|
{{ new Date(record.issuedAt).toLocaleString() }}
|
||||||
<span v-if="record.amountConfirmed" class="tooltip">
|
</td>
|
||||||
<fa icon="circle-check" class="text-green-600 fa-fw ml-1" />
|
<td class="p-1">
|
||||||
<span class="tooltiptext">Confirmed</span>
|
<span v-if="record.agentDid == contact.did">
|
||||||
</span>
|
<div class="font-bold">
|
||||||
<button v-else class="tooltip" @click="confirm(record)">
|
{{ record.amount }} {{ record.unit }}
|
||||||
<fa icon="circle" class="text-blue-600 fa-fw ml-1" />
|
<span v-if="record.amountConfirmed" title="Confirmed">
|
||||||
<span class="tooltiptext">Unconfirmed</span>
|
<fa icon="circle-check" class="text-green-600 fa-fw" />
|
||||||
</button>
|
</span>
|
||||||
</div>
|
<button v-else @click="confirm(record)" title="Unconfirmed">
|
||||||
<br />
|
<fa icon="circle" class="text-blue-600 fa-fw" />
|
||||||
{{ record.description }}
|
</button>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<div class="italic text-xs sm:text-sm text-slate-500">
|
||||||
<div class="w-1/8">
|
{{ record.description }}
|
||||||
<span v-if="record.agentDid == contact.did">
|
</div>
|
||||||
<fa icon="long-arrow-alt-left" class="text-slate-900 fa-fw ml-1" />
|
</span>
|
||||||
</span>
|
</td>
|
||||||
<span v-else>
|
<td class="p-1">
|
||||||
|
<span v-if="record.agentDid == contact.did">
|
||||||
<fa icon="long-arrow-alt-right" class="text-slate-900 fa-fw ml-1" />
|
<fa icon="arrow-left" class="text-slate-400 fa-fw" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span v-else>
|
||||||
<div class="w-1/4">
|
<fa icon="arrow-right" class="text-slate-400 fa-fw" />
|
||||||
<span v-if="record.agentDid != contact.did">
|
</span>
|
||||||
<div class="font-bold">
|
</td>
|
||||||
{{ record.amount }} {{ record.unit }}
|
<td class="p-1">
|
||||||
<span v-if="record.amountConfirmed" class="tooltip">
|
<span v-if="record.agentDid != contact.did">
|
||||||
<fa icon="circle-check" class="text-green-600 fa-fw ml-1" />
|
<div class="font-bold">
|
||||||
<span class="tooltiptext">Confirmed</span>
|
{{ record.amount }} {{ record.unit }}
|
||||||
</span>
|
<span v-if="record.amountConfirmed" title="Confirmed">
|
||||||
<button v-else class="tooltip" @click="cannotConfirmMessage()">
|
<fa icon="circle-check" class="text-green-600 fa-fw" />
|
||||||
<fa icon="circle" class="text-slate-600 fa-fw ml-1" />
|
</span>
|
||||||
<span class="tooltiptext">Unconfirmed</span>
|
<button
|
||||||
</button>
|
v-else
|
||||||
</div>
|
@click="cannotConfirmMessage()"
|
||||||
<br />
|
title="Unconfirmed"
|
||||||
{{ record.description }}
|
>
|
||||||
</span>
|
<fa icon="circle" class="text-slate-600 fa-fw" />
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="italic text-xs sm:text-sm text-slate-500">
|
||||||
|
{{ record.description }}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
:alertTitle="alertTitle"
|
:alertTitle="alertTitle"
|
||||||
:alertMessage="alertMessage"
|
:alertMessage="alertMessage"
|
||||||
@@ -170,7 +194,7 @@ export default class ContactsView extends Vue {
|
|||||||
encodeURIComponent(identity.did) +
|
encodeURIComponent(identity.did) +
|
||||||
"&recipientDid=" +
|
"&recipientDid=" +
|
||||||
encodeURIComponent(contact.did);
|
encodeURIComponent(contact.did);
|
||||||
const headers = this.getHeaders(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
result = resp.data.data;
|
result = resp.data.data;
|
||||||
|
|||||||
@@ -24,8 +24,12 @@
|
|||||||
<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"
|
<span class="grow italic text-slate-500"
|
||||||
><fa icon="question-circle" class="fa-fw fa-xl text-slate-400"></fa>
|
><EntityIcon
|
||||||
|
entityId="Anonymous"
|
||||||
|
:iconSize="32"
|
||||||
|
class="opacity-50 inline-block align-middle border border-dashed border-slate-400 bg-slate-200 rounded-md mr-1"
|
||||||
|
></EntityIcon>
|
||||||
Anonymous
|
Anonymous
|
||||||
</span>
|
</span>
|
||||||
<span class="text-right">
|
<span class="text-right">
|
||||||
@@ -46,7 +50,11 @@
|
|||||||
>
|
>
|
||||||
<h2 class="text-base flex gap-4 items-center">
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
<span class="grow font-semibold"
|
<span class="grow font-semibold"
|
||||||
><fa icon="user" class="fa-fw fa-xl text-slate-400"></fa>
|
><EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="32"
|
||||||
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||||
|
></EntityIcon>
|
||||||
{{ contact.name || "(no name)" }}
|
{{ contact.name || "(no name)" }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-right">
|
<span class="text-right">
|
||||||
@@ -87,9 +95,10 @@ import { Account } from "@/db/tables/accounts";
|
|||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, AlertMessage, QuickNav },
|
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
|||||||
@@ -17,10 +17,6 @@
|
|||||||
:dotsOptions="{ type: 'square' }"
|
:dotsOptions="{ type: 'square' }"
|
||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
/>
|
/>
|
||||||
<AlertMessage
|
|
||||||
:alertTitle="alertTitle"
|
|
||||||
:alertMessage="alertMessage"
|
|
||||||
></AlertMessage>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -32,18 +28,15 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { SimpleSigner } from "@/libs/crypto";
|
import { SimpleSigner } from "@/libs/crypto";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
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;
|
||||||
alertTitle = "";
|
|
||||||
alertMessage = "";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QRCodeVue3,
|
QRCodeVue3,
|
||||||
AlertMessage,
|
|
||||||
QuickNav,
|
QuickNav,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -55,7 +48,10 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
public async getIdentity(activeDid) {
|
public async getIdentity(activeDid) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
const account: Account | undefined = 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) {
|
||||||
@@ -66,15 +62,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHeaders(identity) {
|
|
||||||
const token = await accessToken(identity);
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
|||||||
@@ -66,18 +66,27 @@
|
|||||||
: "Unconfirmed"
|
: "Unconfirmed"
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
|
<br />
|
||||||
|
(Only hours shown)
|
||||||
|
<br />
|
||||||
|
(Only recent shown)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul v-if="contacts.length > 0" class="border-t border-slate-300">
|
<ul v-if="contacts.length > 0" class="border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300 py-4"
|
class="border-b border-slate-300 pt-2.5 pb-4"
|
||||||
v-for="contact in contacts"
|
v-for="contact in contacts"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
>
|
>
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<h2 class="text-base font-semibold">
|
<h2 class="text-base font-semibold">
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="24"
|
||||||
|
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||||
|
></EntityIcon>
|
||||||
{{ contact.name || "(no name)" }}
|
{{ contact.name || "(no name)" }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-sm truncate">{{ contact.did }}</div>
|
<div class="text-sm truncate">{{ contact.did }}</div>
|
||||||
@@ -135,9 +144,12 @@
|
|||||||
<fa icon="trash-can" class="fa-fw" />
|
<fa icon="trash-can" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="showGiveNumbers" class="ml-auto flex gap-1.5">
|
<div
|
||||||
|
v-if="showGiveNumbers && contact.did != activeDid"
|
||||||
|
class="ml-auto flex gap-1.5"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="text-sm uppercase bg-blue-600 text-white px-2 py-1.5 rounded-md"
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
|
||||||
@click="onClickAddGive(activeDid, contact.did)"
|
@click="onClickAddGive(activeDid, contact.did)"
|
||||||
title="givenByMeDescriptions[contact.did]"
|
title="givenByMeDescriptions[contact.did]"
|
||||||
>
|
>
|
||||||
@@ -152,11 +164,11 @@
|
|||||||
: (givenByMeUnconfirmed[contact.did] || 0)
|
: (givenByMeUnconfirmed[contact.did] || 0)
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
}}
|
}}
|
||||||
<fa icon="plus" class="fa-fw" />
|
<fa icon="plus" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="text-sm uppercase bg-blue-600 text-white px-2 py-1.5 rounded-md"
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
|
||||||
@click="onClickAddGive(contact.did, activeDid)"
|
@click="onClickAddGive(contact.did, activeDid)"
|
||||||
title="givenToMeDescriptions[contact.did]"
|
title="givenToMeDescriptions[contact.did]"
|
||||||
>
|
>
|
||||||
@@ -171,7 +183,7 @@
|
|||||||
: (givenToMeUnconfirmed[contact.did] || 0)
|
: (givenToMeUnconfirmed[contact.did] || 0)
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
}}
|
}}
|
||||||
<fa icon="plus" class="fa-fw" />
|
<fa icon="plus" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
@@ -214,12 +226,13 @@ import {
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { AlertMessage, QuickNav },
|
components: { AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ContactsView extends Vue {
|
export default class ContactsView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
@@ -294,20 +307,27 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadGives() {
|
async loadGives() {
|
||||||
const handleResponse = (resp, descriptions, confirmed, unconfirmed) => {
|
const handleResponse = (
|
||||||
|
resp,
|
||||||
|
descriptions,
|
||||||
|
confirmed,
|
||||||
|
unconfirmed,
|
||||||
|
useRecipient,
|
||||||
|
) => {
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const allData = resp.data.data;
|
const allData = resp.data.data;
|
||||||
for (const give of allData) {
|
for (const give of allData) {
|
||||||
|
const otherDid = useRecipient ? give.recipientDid : give.agentDid;
|
||||||
if (give.unit === "HUR") {
|
if (give.unit === "HUR") {
|
||||||
if (give.amountConfirmed) {
|
if (give.amountConfirmed) {
|
||||||
const prevAmount = confirmed[give.agentDid] || 0;
|
const prevAmount = confirmed[otherDid] || 0;
|
||||||
confirmed[give.agentDid] = prevAmount + give.amount;
|
confirmed[otherDid] = prevAmount + give.amount;
|
||||||
} else {
|
} else {
|
||||||
const prevAmount = unconfirmed[give.agentDid] || 0;
|
const prevAmount = unconfirmed[otherDid] || 0;
|
||||||
unconfirmed[give.agentDid] = prevAmount + give.amount;
|
unconfirmed[otherDid] = prevAmount + give.amount;
|
||||||
}
|
}
|
||||||
if (!descriptions[give.agentDid] && give.description) {
|
if (!descriptions[otherDid] && give.description) {
|
||||||
descriptions[give.agentDid] = give.description;
|
descriptions[otherDid] = give.description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,17 +354,15 @@ export default class ContactsView extends Vue {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { headers, identity } = await this.getHeadersAndIdentity(
|
const { headers } = await this.getHeadersAndIdentity(this.activeDid);
|
||||||
this.activeDid,
|
|
||||||
);
|
|
||||||
const givenByUrl =
|
const givenByUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/gives?agentDid=" +
|
"/api/v2/report/gives?agentDid=" +
|
||||||
encodeURIComponent(identity.did);
|
encodeURIComponent(this.activeDid);
|
||||||
const givenToUrl =
|
const givenToUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/gives?recipientDid=" +
|
"/api/v2/report/gives?recipientDid=" +
|
||||||
encodeURIComponent(identity.did);
|
encodeURIComponent(this.activeDid);
|
||||||
|
|
||||||
const [givenByMeResp, givenToMeResp] = await Promise.all([
|
const [givenByMeResp, givenToMeResp] = await Promise.all([
|
||||||
this.axios.get(givenByUrl, { headers }),
|
this.axios.get(givenByUrl, { headers }),
|
||||||
@@ -359,6 +377,7 @@ export default class ContactsView extends Vue {
|
|||||||
givenByMeDescriptions,
|
givenByMeDescriptions,
|
||||||
givenByMeConfirmed,
|
givenByMeConfirmed,
|
||||||
givenByMeUnconfirmed,
|
givenByMeUnconfirmed,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
this.givenByMeDescriptions = givenByMeDescriptions;
|
this.givenByMeDescriptions = givenByMeDescriptions;
|
||||||
this.givenByMeConfirmed = givenByMeConfirmed;
|
this.givenByMeConfirmed = givenByMeConfirmed;
|
||||||
@@ -372,6 +391,7 @@ export default class ContactsView extends Vue {
|
|||||||
givenToMeDescriptions,
|
givenToMeDescriptions,
|
||||||
givenToMeConfirmed,
|
givenToMeConfirmed,
|
||||||
givenToMeUnconfirmed,
|
givenToMeUnconfirmed,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
this.givenToMeDescriptions = givenToMeDescriptions;
|
this.givenToMeDescriptions = givenToMeDescriptions;
|
||||||
this.givenToMeConfirmed = givenToMeConfirmed;
|
this.givenToMeConfirmed = givenToMeConfirmed;
|
||||||
|
|||||||
@@ -83,10 +83,11 @@
|
|||||||
class="block py-4 flex gap-4"
|
class="block py-4 flex gap-4"
|
||||||
>
|
>
|
||||||
<div class="w-12">
|
<div class="w-12">
|
||||||
<img
|
<EntityIcon
|
||||||
src="https://picsum.photos/200/200?random=1"
|
:entityId="project.handleId"
|
||||||
class="w-full rounded"
|
:iconSize="48"
|
||||||
/>
|
class="block border border-slate-300 rounded-md"
|
||||||
|
></EntityIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
@@ -120,9 +121,10 @@ import { didInfo } from "@/libs/endorserServer";
|
|||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { AlertMessage, QuickNav, InfiniteScroll },
|
components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {
|
export default class DiscoverView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
|||||||
@@ -128,6 +128,14 @@
|
|||||||
key.
|
key.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
||||||
|
<p>
|
||||||
|
Go
|
||||||
|
<router-link to="start" class="text-blue-500">
|
||||||
|
create another identity here.
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
I know there is a record from someone, so why can't I see that info?
|
I know there is a record from someone, so why can't I see that info?
|
||||||
</h2>
|
</h2>
|
||||||
@@ -146,14 +154,6 @@
|
|||||||
page.
|
page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
|
||||||
<p>
|
|
||||||
Go
|
|
||||||
<router-link to="start" class="text-blue-500">
|
|
||||||
create another identity here.
|
|
||||||
</router-link>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
||||||
<p>
|
<p>
|
||||||
See
|
See
|
||||||
|
|||||||
@@ -92,6 +92,21 @@
|
|||||||
>
|
>
|
||||||
Danger
|
Danger
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
@@ -99,18 +114,32 @@
|
|||||||
<p class="mb-4">Show appreciation to a contact:</p>
|
<p class="mb-4">Show appreciation to a contact:</p>
|
||||||
|
|
||||||
<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()">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Anonymous
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-for="contact in allContacts"
|
v-for="contact in allContacts"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
@click="openDialog(contact)"
|
@click="openDialog(contact)"
|
||||||
>
|
>
|
||||||
<div class="mb-1">
|
<EntityIcon
|
||||||
<fa icon="user" class="fa-fw fa-xl text-slate-400"></fa>
|
:entityId="contact.did"
|
||||||
</div>
|
:iconSize="64"
|
||||||
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
|
></EntityIcon>
|
||||||
<h3
|
<h3
|
||||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
>
|
>
|
||||||
{{ contact.name || "(no name)" }}
|
{{ contact.name || contact.did }}
|
||||||
</h3>
|
</h3>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -127,6 +156,7 @@
|
|||||||
<!-- If there are no contacts, show this instead: -->
|
<!-- If there are no contacts, show this instead: -->
|
||||||
<div
|
<div
|
||||||
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
|
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.)
|
(No contacts to show.)
|
||||||
</div>
|
</div>
|
||||||
@@ -183,9 +213,10 @@ import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
|
|||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, AlertMessage, QuickNav },
|
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
@@ -347,8 +378,7 @@ export default class HomeView extends Vue {
|
|||||||
claim = claim.claim;
|
claim = claim.claim;
|
||||||
}
|
}
|
||||||
// agent.did is for legacy data, before March 2023
|
// agent.did is for legacy data, before March 2023
|
||||||
const giverDid =
|
const giverDid = claim.agent?.identifier || claim.agent?.did;
|
||||||
claim.agent?.identifier || claim.agent?.did || giveRecord.issuer;
|
|
||||||
const giverInfo = didInfo(
|
const giverInfo = didInfo(
|
||||||
giverDid,
|
giverDid,
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
@@ -387,7 +417,7 @@ export default class HomeView extends Vue {
|
|||||||
handleDialogResult(result) {
|
handleDialogResult(result) {
|
||||||
if (result.action === "confirm") {
|
if (result.action === "confirm") {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.recordGive(result.contact?.did, result.description, result.hours);
|
this.recordGive(result.giver?.did, result.description, result.hours);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,6 +24,24 @@
|
|||||||
v-model="mnemonic"
|
v-model="mnemonic"
|
||||||
/>
|
/>
|
||||||
{{ mnemonic }}
|
{{ mnemonic }}
|
||||||
|
<h3
|
||||||
|
class="text-sm uppercase font-semibold mb-3"
|
||||||
|
@click="showAdvanced = !showAdvanced"
|
||||||
|
>
|
||||||
|
Advanced
|
||||||
|
</h3>
|
||||||
|
<div v-if="showAdvanced">
|
||||||
|
Enter a custom derivation path
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
v-model="derivationPath"
|
||||||
|
/>
|
||||||
|
For previous uPort or Endorser users,
|
||||||
|
<a @click="derivationPath = UPORT_DERIVATION_PATH" class="text-blue-500">
|
||||||
|
click here to use that value.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<button
|
<button
|
||||||
@click="from_mnemonic()"
|
@click="from_mnemonic()"
|
||||||
@@ -44,7 +62,11 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
import {
|
||||||
|
DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
|
deriveAddress,
|
||||||
|
newIdentifier,
|
||||||
|
} from "../libs/crypto";
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
@@ -52,11 +74,14 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ImportAccountView extends Vue {
|
export default class ImportAccountView extends Vue {
|
||||||
|
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
|
||||||
|
|
||||||
mnemonic = "";
|
mnemonic = "";
|
||||||
address = "";
|
address = "";
|
||||||
privateHex = "";
|
privateHex = "";
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
derivationPath = "";
|
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
|
||||||
|
showAdvanced = false;
|
||||||
|
|
||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
@@ -65,8 +90,10 @@ export default class ImportAccountView extends Vue {
|
|||||||
public async from_mnemonic() {
|
public async from_mnemonic() {
|
||||||
const mne: string = this.mnemonic.trim().toLowerCase();
|
const mne: string = this.mnemonic.trim().toLowerCase();
|
||||||
if (this.mnemonic.trim().length > 0) {
|
if (this.mnemonic.trim().length > 0) {
|
||||||
[this.address, this.privateHex, this.publicHex, this.derivationPath] =
|
[this.address, this.privateHex, this.publicHex] = deriveAddress(
|
||||||
deriveAddress(mne);
|
mne,
|
||||||
|
this.derivationPath,
|
||||||
|
);
|
||||||
|
|
||||||
const newId = newIdentifier(
|
const newId = newIdentifier(
|
||||||
this.address,
|
this.address,
|
||||||
|
|||||||
163
src/views/ImportDerivedAccountView.vue
Normal file
163
src/views/ImportDerivedAccountView.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
<!-- Cancel -->
|
||||||
|
<button
|
||||||
|
@click="$router.go(-1)"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left"></fa>
|
||||||
|
</button>
|
||||||
|
Derive from Existing Identity
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<!-- Import Account Form -->
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="text-center text-xl mb-4 font-light">
|
||||||
|
Will increment the maximum derivation path from the existing seed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-if="didArrays.length > 1">
|
||||||
|
Choose existing DIDs from same seed phrase to compute derivation.
|
||||||
|
</p>
|
||||||
|
<ul class="mb-4">
|
||||||
|
<li
|
||||||
|
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
|
||||||
|
v-for="dids in didArrays"
|
||||||
|
:key="dids[0]"
|
||||||
|
@click="switchAccount(dids[0])"
|
||||||
|
>
|
||||||
|
<fa
|
||||||
|
v-if="dids[0] == selectedArrayFirstDid"
|
||||||
|
icon="circle"
|
||||||
|
class="fa-fw text-blue-400 text-xl mr-3"
|
||||||
|
></fa>
|
||||||
|
<fa
|
||||||
|
v-else
|
||||||
|
icon="circle"
|
||||||
|
class="fa-fw text-slate-400 text-xl mr-3"
|
||||||
|
></fa>
|
||||||
|
<span class="overflow-hidden">
|
||||||
|
<div class="text-sm text-slate-500 truncate">
|
||||||
|
<code>{{ dids.join(",") }}</code>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8">
|
||||||
|
<button
|
||||||
|
@click="incrementDerivation()"
|
||||||
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Increment and Import
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="onCancelClick()"
|
||||||
|
type="button"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import {
|
||||||
|
DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
|
deriveAddress,
|
||||||
|
newIdentifier,
|
||||||
|
} from "../libs/crypto";
|
||||||
|
import { accountsDB, db } from "@/db";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {},
|
||||||
|
})
|
||||||
|
export default class ImportAccountView extends Vue {
|
||||||
|
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
|
||||||
|
didArrays: Array<Array<string>> = [];
|
||||||
|
selectedArrayFirstDid = "";
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
const seedDids = {};
|
||||||
|
accounts.forEach((account) => {
|
||||||
|
const prevDids = seedDids[account.mnemonic] || [];
|
||||||
|
seedDids[account.mnemonic] = prevDids.concat([account.did]);
|
||||||
|
});
|
||||||
|
this.didArrays = Object.values(seedDids);
|
||||||
|
this.selectedArrayFirstDid = this.didArrays[0][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCancelClick() {
|
||||||
|
this.$router.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public switchAccount(did: string) {
|
||||||
|
this.selectedArrayFirstDid = did;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async incrementDerivation() {
|
||||||
|
await accountsDB.open();
|
||||||
|
// find the maximum derivation path for the selected DIDs
|
||||||
|
const selectedArray: Array<string> = this.didArrays.find(
|
||||||
|
(dids) => dids[0] === this.selectedArrayFirstDid,
|
||||||
|
);
|
||||||
|
const allMatchingAccounts = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.anyOf(...selectedArray)
|
||||||
|
.toArray();
|
||||||
|
const accountWithMaxDeriv = allMatchingAccounts[0];
|
||||||
|
allMatchingAccounts.slice(1).forEach((account) => {
|
||||||
|
if (account.derivationPath > accountWithMaxDeriv.derivationPath) {
|
||||||
|
accountWithMaxDeriv.derivationPath = account.derivationPath;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// increment the last number in that max derivation path
|
||||||
|
let lastStr = accountWithMaxDeriv.derivationPath.split("/").slice(-1)[0];
|
||||||
|
if (lastStr.endsWith("'")) {
|
||||||
|
lastStr = lastStr.slice(0, -1);
|
||||||
|
}
|
||||||
|
const lastNum = parseInt(lastStr, 10);
|
||||||
|
const newLastNum = lastNum + 1;
|
||||||
|
const newDerivPath = accountWithMaxDeriv.derivationPath
|
||||||
|
.split("/")
|
||||||
|
.slice(0, -1)
|
||||||
|
.concat([newLastNum.toString() + "'"])
|
||||||
|
.join("/");
|
||||||
|
|
||||||
|
const mne: string = accountWithMaxDeriv.mnemonic;
|
||||||
|
|
||||||
|
const [address, privateHex, publicHex] = deriveAddress(mne, newDerivPath);
|
||||||
|
|
||||||
|
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await accountsDB.accounts.add({
|
||||||
|
dateCreated: new Date().toISOString(),
|
||||||
|
derivationPath: newDerivPath,
|
||||||
|
did: newId.did,
|
||||||
|
identity: JSON.stringify(newId),
|
||||||
|
mnemonic: mne,
|
||||||
|
publicKeyHex: newId.keys[0].publicKeyHex,
|
||||||
|
});
|
||||||
|
|
||||||
|
// record that as the active DID
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: newId.did,
|
||||||
|
});
|
||||||
|
this.$router.push({ name: "account" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error saving mnemonic & updating settings:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -39,6 +39,40 @@
|
|||||||
{{ description.length }}/500 max. characters
|
{{ description.length }}/500 max. characters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="mr-2"
|
||||||
|
v-model="includeLocation"
|
||||||
|
@change="includeLocation = true"
|
||||||
|
/>
|
||||||
|
<label for="includeLocation">Include Location</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
||||||
|
<l-map
|
||||||
|
ref="map"
|
||||||
|
v-model:zoom="zoom"
|
||||||
|
:center="[0, 0]"
|
||||||
|
@click="
|
||||||
|
(event) => {
|
||||||
|
latitude = event.latlng.lat;
|
||||||
|
longitude = event.latlng.lng;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="latitude || longitude"
|
||||||
|
:lat-lng="[latitude, longitude]"
|
||||||
|
@click="maybeEraseLatLong()"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<button
|
<button
|
||||||
:disabled="isHiddenSave"
|
:disabled="isHiddenSave"
|
||||||
@@ -71,9 +105,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
@@ -83,17 +119,21 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { AlertMessage },
|
components: { AlertMessage, LMap, LMarker, LTileLayer },
|
||||||
})
|
})
|
||||||
export default class NewEditProjectView extends Vue {
|
export default class NewEditProjectView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
|
||||||
projectName = "";
|
|
||||||
description = "";
|
|
||||||
errorMessage = "";
|
|
||||||
numAccounts = 0;
|
|
||||||
alertTitle = "";
|
alertTitle = "";
|
||||||
alertMessage = "";
|
alertMessage = "";
|
||||||
|
apiServer = "";
|
||||||
|
description = "";
|
||||||
|
errorMessage = "";
|
||||||
|
includeLocation = false;
|
||||||
|
latitude = 0;
|
||||||
|
longitude = 0;
|
||||||
|
numAccounts = 0;
|
||||||
|
projectName = "";
|
||||||
|
zoom = 2;
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
@@ -185,6 +225,15 @@ export default class NewEditProjectView extends Vue {
|
|||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
vcClaim.identifier = this.projectId;
|
vcClaim.identifier = this.projectId;
|
||||||
}
|
}
|
||||||
|
if (this.includeLocation) {
|
||||||
|
vcClaim.location = {
|
||||||
|
geo: {
|
||||||
|
"@type": "GeoCoordinates",
|
||||||
|
latitude: this.latitude,
|
||||||
|
longitude: this.longitude,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
@@ -218,13 +267,13 @@ export default class NewEditProjectView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
// handleId is new in server v release-1.6.0; remove fullIri when that
|
||||||
// version shows up here: https://endorser.ch:3000/api-docs/
|
// version shows up here: https://api.endorser.ch/api-docs/
|
||||||
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
||||||
this.errorMessage = "";
|
this.errorMessage = "";
|
||||||
this.alertTitle = "";
|
this.alertTitle = "";
|
||||||
this.alertMessage = "";
|
this.alertMessage = "";
|
||||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
// handleId is new in server v release-1.6.0; remove fullIri when that
|
||||||
// version shows up here: https://endorser.ch:3000/api-docs/
|
// version shows up here: https://api.endorser.ch/api-docs/
|
||||||
useAppStore().setProjectId(
|
useAppStore().setProjectId(
|
||||||
resp.data.success.handleId || resp.data.success.fullIri,
|
resp.data.success.handleId || resp.data.success.fullIri,
|
||||||
);
|
);
|
||||||
@@ -299,6 +348,14 @@ export default class NewEditProjectView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public maybeEraseLatLong() {
|
||||||
|
if (window.confirm("Are you sure you don't want to mark a location?")) {
|
||||||
|
this.latitude = 0;
|
||||||
|
this.longitude = 0;
|
||||||
|
this.includeLocation = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,6 @@
|
|||||||
>
|
>
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
<!-- Context Menu -->
|
|
||||||
<a
|
|
||||||
href=""
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -right-2 -top-1"
|
|
||||||
><fa icon="ellipsis-vertical" class="fa-fw"></fa
|
|
||||||
></a>
|
|
||||||
|
|
||||||
View Plan
|
View Plan
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,16 +19,28 @@
|
|||||||
<!-- Project Details -->
|
<!-- Project Details -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold">{{ name }}</h2>
|
<div class="block pb-4 flex gap-4">
|
||||||
<div class="flex justify-between gap-4 text-sm mb-3">
|
<div class="flex-none w-16 pt-1">
|
||||||
<span
|
<EntityIcon
|
||||||
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
:entityId="projectId"
|
||||||
{{ issuer }}</span
|
:iconSize="64"
|
||||||
>
|
class="block border border-slate-300 rounded-md"
|
||||||
<span
|
></EntityIcon>
|
||||||
><fa icon="calendar" class="fa-fw text-slate-400"></fa
|
</div>
|
||||||
>{{ timeSince }}
|
|
||||||
</span>
|
<div class="overflow-hidden">
|
||||||
|
<h2 class="text-xl font-semibold">{{ name }}</h2>
|
||||||
|
<div class="text-sm mb-3">
|
||||||
|
<div class="truncate">
|
||||||
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ issuer }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ timeSince }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-slate-500">
|
<div class="text-sm text-slate-500">
|
||||||
@@ -56,6 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
v-if="issuer == activeDid"
|
||||||
type="button"
|
type="button"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
@click="onEditClick()"
|
@click="onEditClick()"
|
||||||
@@ -65,93 +71,119 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div v-if="activeDid">
|
<div v-if="activeDid" class="text-center">
|
||||||
<button
|
<button
|
||||||
@click="openDialog({ name: 'you', did: activeDid })"
|
@click="openDialog({ name: 'you', did: activeDid })"
|
||||||
class="text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
I gave...
|
I gave…
|
||||||
</button>
|
</button>
|
||||||
― or:
|
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- similar contact selection code is in multiple places -->
|
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
|
||||||
Record a gift from
|
|
||||||
<span v-for="contact in allContacts" :key="contact.did">
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
<button @click="openDialog(contact)" class="text-blue-500">
|
<li @click="openDialog()">
|
||||||
{{ contact.name }}</button
|
<EntityIcon
|
||||||
>,
|
:entityId="Anonymous"
|
||||||
</span>
|
:iconSize="64"
|
||||||
<span v-if="allContacts.length > 0"> or </span>
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
<button @click="openDialog()" class="text-blue-500">
|
></EntityIcon>
|
||||||
someone not specified
|
<h3
|
||||||
</button>
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
Anonymous
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="contact in allContacts"
|
||||||
|
:key="contact.did"
|
||||||
|
@click="openDialog(contact)"
|
||||||
|
>
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="64"
|
||||||
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
|
></EntityIcon>
|
||||||
|
<h3
|
||||||
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
{{ contact.name || "(no name)" }}
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||||
|
<router-link
|
||||||
|
v-if="allContacts.length > 7"
|
||||||
|
:to="{ name: 'contact-gives' }"
|
||||||
|
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Show More Contacts…
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gifts to & from this -->
|
<!-- Gifts to & from this -->
|
||||||
<div class="mt-8 flex justify-around">
|
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h1 class="text-xl">Given to this Project</h1>
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
|
Given to this Project
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ul class="text-sm border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
v-for="give in givesToThis"
|
||||||
|
:key="give.id"
|
||||||
|
class="py-1.5 border-b border-slate-300"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span
|
||||||
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="give.amount"
|
||||||
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.amount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="give.description" class="text-slate-500">
|
||||||
|
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.description }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h1 class="text-xl">... and paid forward from this Project</h1>
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
</div>
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
</div>
|
…and from this Project
|
||||||
<div class="flex justify-around">
|
</h3>
|
||||||
<div class="w-1/2">
|
|
||||||
<div v-for="give in givesToThis" :key="give.id">
|
<ul class="text-sm border-t border-slate-300">
|
||||||
<div class="flex justify-between">
|
<li
|
||||||
<div class="flex gap-3">
|
v-for="give in givesByThis"
|
||||||
<div class="flex gap-2">
|
:key="give.id"
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
class="py-1.5 border-b border-slate-300"
|
||||||
<span>{{
|
>
|
||||||
didInfo(give.agentDid, activeDid, allMyDids, allContacts)
|
<div class="flex justify-between gap-4">
|
||||||
}}</span>
|
<span
|
||||||
</div>
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
<div class="flex gap-2" v-if="give.amount">
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
<fa
|
</span>
|
||||||
icon="clock"
|
<span v-if="give.amount"
|
||||||
v-if="give.unit === 'HUR'"
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
class="fa-fw text-slate-400"
|
{{ give.amount }}
|
||||||
></fa>
|
</span>
|
||||||
<fa icon="coins" v-else class="fa-fw text-slate-400"></fa>
|
</div>
|
||||||
<span>{{ give.amount }}</span>
|
<div v-if="give.description" class="text-slate-500">
|
||||||
</div>
|
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||||
<div class="flex gap-2" v-if="give.description">
|
{{ give.description }}
|
||||||
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
</div>
|
||||||
<span>{{ give.description }}</span>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-1/2">
|
|
||||||
<div v-for="give in givesByThis" :key="give.id">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
|
||||||
<span>{{
|
|
||||||
didInfo(give.agentDid, activeDid, allMyDids, allContacts)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2" v-if="give.amount">
|
|
||||||
<fa
|
|
||||||
icon="clock"
|
|
||||||
v-if="give.unit === 'HUR'"
|
|
||||||
class="fa-fw text-slate-400"
|
|
||||||
></fa>
|
|
||||||
<fa icon="coins" v-else class="fa-fw text-slate-400"></fa>
|
|
||||||
<span>{{ give.amount }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
|
||||||
<span>{{ give.description }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiftedDialog
|
<GiftedDialog
|
||||||
ref="customDialog"
|
ref="customDialog"
|
||||||
@dialog-result="handleDialogResult"
|
@dialog-result="handleDialogResult"
|
||||||
@@ -183,9 +215,10 @@ import {
|
|||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, AlertMessage, QuickNav },
|
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ProjectViewView extends Vue {
|
export default class ProjectViewView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
|||||||
@@ -50,10 +50,11 @@
|
|||||||
class="block py-4 flex gap-4"
|
class="block py-4 flex gap-4"
|
||||||
>
|
>
|
||||||
<div class="flex-none w-12">
|
<div class="flex-none w-12">
|
||||||
<img
|
<EntityIcon
|
||||||
src="https://picsum.photos/200/200?random=1"
|
:entityId="project.handleId"
|
||||||
class="w-full rounded"
|
:iconSize="48"
|
||||||
/>
|
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||||
|
></EntityIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
@@ -82,9 +83,10 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { InfiniteScroll, AlertMessage, QuickNav },
|
components: { InfiniteScroll, AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ProjectsView extends Vue {
|
export default class ProjectsView extends Vue {
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
|||||||
@@ -20,22 +20,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeAccount">
|
<div v-if="activeAccount">
|
||||||
<p>
|
<p class="text-center mb-4">
|
||||||
BEWARE: Anyone who gets hold of this mnemonic seed phrase will be able
|
<b class="text-red-600">BEWARE!</b> Anyone who has this seed phrase will
|
||||||
impersonate you and take over any digital holdings based on it. So only
|
be able impersonate you and take over any digital holdings based on it.
|
||||||
reveal it when you are in a private place out of sight of other eyes,
|
Reveal it when you are somewhere only you can see your screen, and
|
||||||
and only record it in something private -- don't take a screenshot or
|
record it somewhere only you have access.
|
||||||
send it to any online service.
|
<i>Don't take a screenshot or send it to any online service.</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<p v-if="numAccounts > 1">
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
<b class="text-orange-600">Note:</b> You have more than one identity
|
||||||
@click="showSeedPhrase"
|
stored in this browser. If they are all based on the same seed as the
|
||||||
>
|
current identity, this one backup is sufficient; however, if you have
|
||||||
Click here when you're ready to see it.
|
different seeds for other identities, you will have to back them up
|
||||||
</button>
|
separately.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p v-if="showSeed">{{ activeAccount.mnemonic }}</p>
|
<div class="bg-slate-100 rounded-md overflow-hidden p-4 mb-4">
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
|
@click="showSeedPhrase"
|
||||||
|
>
|
||||||
|
Reveal my Seed Phrase
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p v-if="showSeed" class="text-center text-slate-700 mt-2">
|
||||||
|
{{ activeAccount.mnemonic }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>You do not have an active identity.</div>
|
<div v-else>You do not have an active identity.</div>
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
@@ -56,6 +68,7 @@ import QuickNav from "@/components/QuickNav";
|
|||||||
@Component({ components: { AlertMessage, QuickNav } })
|
@Component({ components: { AlertMessage, QuickNav } })
|
||||||
export default class SeedBackupView extends Vue {
|
export default class SeedBackupView extends Vue {
|
||||||
activeAccount = null;
|
activeAccount = null;
|
||||||
|
numAccounts = 0;
|
||||||
showSeed = false;
|
showSeed = false;
|
||||||
alertMessage = "";
|
alertMessage = "";
|
||||||
alertTitle = "";
|
alertTitle = "";
|
||||||
@@ -69,6 +82,7 @@ export default class SeedBackupView extends Vue {
|
|||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
this.numAccounts = accounts.length;
|
||||||
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Got an error loading an identity:", err);
|
console.error("Got an error loading an identity:", err);
|
||||||
|
|||||||
@@ -10,30 +10,46 @@
|
|||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<p class="text-center text-xl mb-4 font-light">
|
<p class="text-center text-xl mb-4 font-light">
|
||||||
Do you already have an identity to import?
|
Do you have an identity to import?
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
@click="onClickYes()"
|
@click="onClickYes()"
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
class="block w-full text-center text-lg uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@click="onClickNo()"
|
@click="onClickNo()"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
||||||
>Yes</a
|
|
||||||
>
|
>
|
||||||
|
Yes
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="numAccounts > 0"
|
||||||
|
@click="onClickDerive()"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
||||||
|
>
|
||||||
|
Derive New Address from Seed Imported Previously
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { accountsDB } from "@/db";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class StartView extends Vue {
|
export default class StartView extends Vue {
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
public onClickYes() {
|
public onClickYes() {
|
||||||
this.$router.push({ name: "new-identifier" });
|
this.$router.push({ name: "new-identifier" });
|
||||||
}
|
}
|
||||||
@@ -41,5 +57,9 @@ export default class StartView extends Vue {
|
|||||||
public onClickNo() {
|
public onClickNo() {
|
||||||
this.$router.push({ name: "import-account" });
|
this.$router.push({ name: "import-account" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onClickDerive() {
|
||||||
|
this.$router.push({ name: "import-derive" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
316
web-push.md
Normal file
316
web-push.md
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
Web Push notifications is a web browser messaging protocol defined by the W3C.
|
||||||
|
|
||||||
|
Discussions of this interesting technology are clouded because of a
|
||||||
|
terminological morass.
|
||||||
|
|
||||||
|
To understand how Web Push operates, we need to observe that are three (and
|
||||||
|
potentially four) parties involved. These are:
|
||||||
|
|
||||||
|
1) The user's web browser. Let's call that BROWSER
|
||||||
|
2) The Web Push Service Provider which is operated by the organization
|
||||||
|
controlling the web browser's source code. Here named PROVIDER. An example of a
|
||||||
|
PROVIDER is FCM (Firebase Cloud Messaging) which is owned by Google.
|
||||||
|
3) The Web Application that a user is visiting from their web browser. Let's
|
||||||
|
call this the SERVICE (short for Web Push application service)
|
||||||
|
4) A Custom Web Push Intermediary Service, either third party or self-hosted.
|
||||||
|
Called INTERMEDIARY here. FCM also may fit in this category if the SERVICE
|
||||||
|
has an API key from FCM.]
|
||||||
|
|
||||||
|
The workflow works like this:
|
||||||
|
|
||||||
|
BROWSER visits a website which hosts a SERVICE.
|
||||||
|
|
||||||
|
The SERVICE asks BROWSER for its permission to subscribe to messages coming
|
||||||
|
from the SERVICE.
|
||||||
|
|
||||||
|
The SERVICE will provide context and obtain explicit permission before prompting
|
||||||
|
for notification permission:
|
||||||
|
|
||||||
|
In order to provide this context and explict permission a two-step opt-in process
|
||||||
|
where the user is first presented with a pre-permission dialog box that explains
|
||||||
|
what the notifications are for and why they are useful. This may help reduce the
|
||||||
|
possibility of users clicking "don't allow".
|
||||||
|
|
||||||
|
Now, to explain what happens in Typescript, we can activate a browser's
|
||||||
|
permission dialogue in this manner:
|
||||||
|
|
||||||
|
```
|
||||||
|
function askPermission(): Promise<NotificationPermission> {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const permissionResult = Notification.requestPermission(function(result) {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permissionResult) {
|
||||||
|
permissionResult.then(resolve, reject);
|
||||||
|
}
|
||||||
|
}).then(function(permissionResult) {
|
||||||
|
if (permissionResult !== 'granted') {
|
||||||
|
throw new Error("We weren't granted permission.");
|
||||||
|
}
|
||||||
|
return permissionResult;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The Notification.permission property indicates the permission level for the
|
||||||
|
current session and returns one of the following string values:
|
||||||
|
|
||||||
|
'granted': The user has granted permission for notifications.
|
||||||
|
'denied': The user has denied permission for notifications.
|
||||||
|
'default': The user has not made a choice yet.
|
||||||
|
|
||||||
|
Once the user has granted permission, the client application registers a service
|
||||||
|
worker using the `ServiceWorkerRegistration` API.
|
||||||
|
|
||||||
|
The `ServiceWorkerRegistration` API is accessible via the browser's `navigator`
|
||||||
|
object and the `navigator.serviceWorker` child object and ultimately directly
|
||||||
|
accessible via the navigator.serviceWorker.register method which also creates
|
||||||
|
the service worker or the navigator.serviceWorker.getRegistration method.
|
||||||
|
|
||||||
|
Once you have a `ServiceWorkerRegistration` object, that object will provide a
|
||||||
|
child object named `pushManager` through which subscription and management of
|
||||||
|
subscriptions may be done.
|
||||||
|
|
||||||
|
Let's go through the `register` method first:
|
||||||
|
|
||||||
|
```
|
||||||
|
navigator.serviceWorker.register('sw.js', { scope: '/' })
|
||||||
|
.then(function(registration) {
|
||||||
|
console.log('Service worker registered successfully:', registration);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.log('Service worker registration failed:', error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `sw.js` file contains the logic for what a service worker should do.
|
||||||
|
It executes in a separate thread of execution from the web page but provides a
|
||||||
|
means of communicating between itself and the web page via messages.
|
||||||
|
|
||||||
|
Note that there is a scope can specify what network requests it may
|
||||||
|
intercept.
|
||||||
|
|
||||||
|
The Vue project already has its own service worker but it is possible to
|
||||||
|
create multiple service worker files by registering them on different scopes.
|
||||||
|
|
||||||
|
It is useful architecturally to specify a separate server worker file.
|
||||||
|
|
||||||
|
In the case of web push, the path of the scope only has reference to the domain
|
||||||
|
of the service worker and no relationship to the pathing for the web push
|
||||||
|
server. In order to specify more than one server workers each needs to be on
|
||||||
|
different scope paths!
|
||||||
|
|
||||||
|
Here's a version which can be used for testing locally. Note there can be
|
||||||
|
caching issues in your browser! Incognito is highly recommended.
|
||||||
|
|
||||||
|
sw-dev.ts
|
||||||
|
```
|
||||||
|
self.addEventListener('push', function(event: PushEvent) {
|
||||||
|
console.log('Received a push message', event);
|
||||||
|
|
||||||
|
const title = 'Push message';
|
||||||
|
const body = 'The message body';
|
||||||
|
const icon = '/images/icon-192x192.png';
|
||||||
|
const tag = 'simple-push-demo-notification-tag';
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: icon,
|
||||||
|
tag: tag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
vue.config.js
|
||||||
|
```
|
||||||
|
module.exports = {
|
||||||
|
pwa: {
|
||||||
|
workboxOptions: {
|
||||||
|
importScripts: ['sw-dev.ts']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once we have the service worker registered and the ServiceWorkerRegistration is
|
||||||
|
returned, we then have access to a `pushManager` property object. This property
|
||||||
|
allows us to continue with the web push work flow.
|
||||||
|
|
||||||
|
In the next step, BROWSER requests a data structure from SERVICE called a VAPID
|
||||||
|
(Voluntary Application Server Identification) which is the public key from a
|
||||||
|
key-pair.
|
||||||
|
|
||||||
|
The VAPID is a specification used to identify the application server (i.e. the
|
||||||
|
SERVICE server) that is sending push messages through a push PROVIDER. It's an
|
||||||
|
authentication mechanism that allows the server to demonstrate its identity to
|
||||||
|
the push PROVIDER, by use of a public and private key pair. These keys are used
|
||||||
|
by the SERVICE in encrypting messages being sent to the BROWSER, as well as
|
||||||
|
being used by the BROWSER in decrypting the messages coming from the SERVICE.
|
||||||
|
|
||||||
|
The VAPID (Voluntary Application Server Identification) key provides more
|
||||||
|
security and authenticity for web push notifications in the following ways:
|
||||||
|
|
||||||
|
Identifying the Application Server:
|
||||||
|
|
||||||
|
The VAPID key is used to identify the application server that is sending
|
||||||
|
the push notifications. This ensures that the push notifications are
|
||||||
|
authentic and not sent by a malicious third party.
|
||||||
|
|
||||||
|
Encrypting the Messages:
|
||||||
|
|
||||||
|
The VAPID key is used to sign the push notifications sent by the
|
||||||
|
application server, ensuring that they are not tampered with during
|
||||||
|
transmission. This provides an additional layer of security and
|
||||||
|
authenticity for the push notifications.
|
||||||
|
|
||||||
|
Adding Contact Information:
|
||||||
|
|
||||||
|
The VAPID key allows a web application to add contact information to
|
||||||
|
the push messages sent to the browser push service. This enables the
|
||||||
|
push service to contact the application server in case of need or
|
||||||
|
provide additional debug information about the push messages.
|
||||||
|
|
||||||
|
Improving Delivery Rates:
|
||||||
|
|
||||||
|
Using the VAPID key can help improve the overall performance of web push
|
||||||
|
notifications, specifically improving delivery rates. By streamlining the
|
||||||
|
delivery process, the chance of delivery errors along the way is lessened.
|
||||||
|
|
||||||
|
If the BROWSER accepts and grants permission to subscribe to receiving from the
|
||||||
|
SERVICE Web Push messages, then the BROWSER makes a subscription request to
|
||||||
|
PROVIDER which creates and stores a special URL for that BROWSER.
|
||||||
|
|
||||||
|
Here's a bit of code describing the above process:
|
||||||
|
|
||||||
|
```
|
||||||
|
// b64 is the VAPID
|
||||||
|
b64 = 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U';
|
||||||
|
const applicationServerKey = urlBase64ToUint8Array(b64);
|
||||||
|
const options: PushSubscriptionOptions = {
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: applicationServerKey
|
||||||
|
};
|
||||||
|
|
||||||
|
registration.pushManager.subscribe(options)
|
||||||
|
.then(function(subscription) {
|
||||||
|
console.log('Push subscription successful:', subscription);
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Push subscription failed:', error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the `applicationServerKey` variable contains the VAPID public
|
||||||
|
key, which is converted to a `Uint8Array` using a function such as this:
|
||||||
|
|
||||||
|
```
|
||||||
|
export function toUint8Array(base64String: string, atobFn: typeof atob): Uint8Array {
|
||||||
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
|
||||||
|
const rawData = atobFn(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The options object is of type `PushSubscriptionOptions`, which includes the
|
||||||
|
`userVisibleOnly` and `applicationServerKey` (ie VAPID public key) properties.
|
||||||
|
|
||||||
|
options: An object that contains the options used for creating the
|
||||||
|
subscription. This object itself has the following sub-properties:
|
||||||
|
|
||||||
|
applicationServerKey: A public key your push service uses for application
|
||||||
|
server identification. This is normally a Uint8Array.
|
||||||
|
|
||||||
|
userVisibleOnly: A boolean value indicating that the push messages that
|
||||||
|
are sent should be made visible to the user through a notification.
|
||||||
|
This is often set to true.
|
||||||
|
|
||||||
|
The subscribe() method returns a `Promise` that resolves to a `PushSubscription`
|
||||||
|
object containing details of the subscription, such as the endpoint URL and the
|
||||||
|
public key. The returned data would have a form like this:
|
||||||
|
|
||||||
|
{
|
||||||
|
"endpoint": "https://some.pushservice.com/some/unique/identifier",
|
||||||
|
"expirationTime": null,
|
||||||
|
"keys": {
|
||||||
|
"p256dh": "some_base64_encoded_string",
|
||||||
|
"auth": "some_other_base64_encoded_string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint: A string representing the endpoint URL for the push service. This
|
||||||
|
URL is essentially the push service address to which the push message would
|
||||||
|
be sent for this particular subscription.
|
||||||
|
|
||||||
|
expirationTime: A DOMHighResTimeStamp (which is basically a number or null)
|
||||||
|
representing the subscription's expiration time in milliseconds since
|
||||||
|
01 January, 1970 UTC. This can be null if the subscription never expires.
|
||||||
|
|
||||||
|
The BROWSER will, internally, then use that URL to check for incoming messages
|
||||||
|
by way of the service worker we described earlier. The BROWSER also sends this
|
||||||
|
URL back to SERVICE which will use that URL to send messages to the BROWSER via
|
||||||
|
the PROVIDER.
|
||||||
|
|
||||||
|
Ultimately, the actual internal process of receiving messages varies from BROWSER
|
||||||
|
to BROWSER. Approaches vary from long-polling HTTP connections to WebSockets. A
|
||||||
|
lot of handwaving and voodoo magic. The bottom line is that the BROWSER itself
|
||||||
|
manages the connection to the PROVIDER whilst the SERVICE must send messages
|
||||||
|
via the PROVIDER so that they reach the BROWSER service worker.
|
||||||
|
|
||||||
|
Just to remind us that in our service worker our code for receiving messages
|
||||||
|
will look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
self.addEventListener('push', function(event: PushEvent) {
|
||||||
|
console.log('Received a push message', event);
|
||||||
|
|
||||||
|
const title = 'Push message';
|
||||||
|
const body = 'The message body';
|
||||||
|
const icon = '/images/icon-192x192.png';
|
||||||
|
const tag = 'simple-push-demo-notification-tag';
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: icon,
|
||||||
|
tag: tag
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Now to address the issue of receiving notification messages on mobile devices.
|
||||||
|
It should be noted that Web Push messages are only received when BROWSER is
|
||||||
|
open, except in the cases of Chrome and Firefox mobile BROWSERS. In iOS, the
|
||||||
|
mobile application (in our case a PWA) must be added to the Home Screen and
|
||||||
|
permissions must be explicitly granted that allow the application to receive
|
||||||
|
push notifications. Further, with an iOS device the user must enable wake on
|
||||||
|
notification to have their device light-up when it receives a notification
|
||||||
|
(https://support.apple.com/enus/HT208081).
|
||||||
|
|
||||||
|
So what about #4? - The INTERMEDIARY. Well, It is possible under very special
|
||||||
|
circumstances to create your own Web Push PROVIDER. The only case I've found so
|
||||||
|
far relates to making an Android Custom ROM. (An Android Custom ROM is a
|
||||||
|
customized version of the Android Operating System.) There are open source
|
||||||
|
IMTERMEDIARY products such as UnifiedPush (https://unifiedpush.org/) which can
|
||||||
|
fulfill this role. If you are using iOS you are not permitted to make or use
|
||||||
|
your own custom Web Push PROVIDER. Apple will never allow anyone to do that.
|
||||||
|
Apple has none of its own.
|
||||||
|
|
||||||
|
It is, however, possible to have a sort of proxy working between your SERVICE
|
||||||
|
and FCM (or iOS). Services that mash up various Push notification services (like
|
||||||
|
OneSignal) can perform in the role of such proxies.
|
||||||
|
|
||||||
|
#4 -The INTERMEDIARY- doesn't appear to be anything we should be spending our
|
||||||
|
time on.
|
||||||
Reference in New Issue
Block a user