Compare commits
89 Commits
design-twe
...
0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 525d3fc15a | |||
| 68f3b79983 | |||
| 5353fe770a | |||
| 60fec5763d | |||
| aeb1d6a6a5 | |||
| ec6175a550 | |||
| c1361e088f | |||
| a2c986951e | |||
| dce7b8e3d9 | |||
| 211e0487fe | |||
| cc931dcb04 | |||
| bfe14cc9c2 | |||
| 275dba4468 | |||
| 1f05e81b05 | |||
| e9ad68f2a5 | |||
| 934664b9c9 | |||
| 780be59c76 | |||
| 4a0bedb628 | |||
| 5689f95230 | |||
| 3083bb084a | |||
| 821d27a58a | |||
|
|
998a1d312f | ||
| 1f13bf772c | |||
| def744b3df | |||
| 0fb37acb24 | |||
| 20bb723f0b | |||
| d821a7bd59 | |||
| 9f3b7314e8 | |||
| 4df7bb58a4 | |||
| 15ccd2394f | |||
| 920c7bb612 | |||
| 6eb26ea90c | |||
| 28b6d9bbf9 | |||
| 7a099183ae | |||
| 11070755d6 | |||
| c79dfac1fe | |||
| 2b06c64664 | |||
| 769a928b3d | |||
| d26d1d3601 | |||
| 1e6159869f | |||
| 75d15ddeb9 | |||
| 051a0a97d8 | |||
| f8d3fe2ee1 | |||
| 4f0a046723 | |||
| c4a0458c08 | |||
| 25b1598fcb | |||
| ddbb700c34 | |||
| fd8877900b | |||
| 05c6ddda02 | |||
| 853eb3c623 | |||
| 44cfe0d88e | |||
| 7fe256dc9e | |||
| e739d0be7c | |||
| 8d873b51bd | |||
| d7f4acb702 | |||
| f8002c4550 | |||
| d6b1386741 | |||
| 50fdd95c60 | |||
| 91c6c7c11c | |||
| 4e28dc8de6 | |||
| fb425f0d51 | |||
| a19aebcb37 | |||
| d0697c1ef4 | |||
| 1dd2333624 | |||
|
|
b4b78f6a2c | ||
|
|
3c0f6ce0de | ||
| 5534f8fa50 | |||
| a5004d475e | |||
| b445b1234f | |||
| 17c96dd01a | |||
| 6ad17101b2 | |||
| b4085ffaa7 | |||
| 4f2cb55753 | |||
| ebf9164ecc | |||
| 540cc21839 | |||
| c182068901 | |||
| aaa1f31945 | |||
| 17c632eb16 | |||
| 41c4cbe61a | |||
| c8402797ad | |||
| 4a09b9b9b1 | |||
| 5db3423301 | |||
| 2b00b243e8 | |||
| f2e5d8168d | |||
| 1d262b8da9 | |||
| 8ed74b71f2 | |||
| 8fb21c3d89 | |||
| 8dbfcd38d3 | |||
| 04df0d4eff |
49
CHANGELOG.md
@@ -9,7 +9,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
## [0.1.6]
|
## [0.2.2] - 2024.01.05
|
||||||
|
### Added
|
||||||
|
- Check for notification capability on front screen
|
||||||
|
- Contact next-public-key-hash in manual textual input
|
||||||
|
- Confirmation for contact visibility change
|
||||||
|
- YAML rendering of full claim details
|
||||||
|
- Hints for onboarding on the contact screen
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.0] - 2024.01.04
|
||||||
|
### Added
|
||||||
|
- Contact next-public-key-hash
|
||||||
|
- Icon for Android
|
||||||
|
- More thorough messaging and testing for notifications
|
||||||
|
|
||||||
|
## [0.1.9] - 2024.01.01
|
||||||
|
### Added
|
||||||
|
- Import for contacts and settings
|
||||||
|
- Second download button for DuckDuckGo
|
||||||
|
### Changed
|
||||||
|
- Removed some keys from Dexie's IndexedDB declarations
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.8] - 2023.12.27- d26d1d360152a7d0e559b68486e85b72b88bd9ff
|
||||||
|
### Added
|
||||||
|
- DB logging for service-worker events
|
||||||
|
- Help page for notifications
|
||||||
|
- Test notification & web-push triggers inside app
|
||||||
|
- Check that the app is installed
|
||||||
|
### Fixed
|
||||||
|
- Project issuer display name
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
|
||||||
|
### Changed
|
||||||
|
- Icons
|
||||||
|
### Fixed
|
||||||
|
- Notification switch now shows message
|
||||||
|
- Prod/test server warning message at top of page
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
|
||||||
|
### Added
|
||||||
|
- Infinite scroll on home page
|
||||||
|
### Changed
|
||||||
|
- UI improvements
|
||||||
|
- Show web-push subscription info
|
||||||
|
- Icon
|
||||||
|
|
||||||
|
|
||||||
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
|
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
|
||||||
|
|||||||
27
README.md
@@ -22,23 +22,25 @@ npm run lint
|
|||||||
|
|
||||||
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||||
|
|
||||||
* `npx prettier --write ./sw_scripts/`
|
* Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit.
|
||||||
|
|
||||||
...to make sure the service worker scripts are in proper form
|
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
|
||||||
|
|
||||||
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit. Tag wth the new version: `git tag 0.1.0`
|
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test". Also record what version is on production.
|
||||||
|
|
||||||
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be PROD.
|
|
||||||
|
|
||||||
* `npm run build`
|
* `npm run build`
|
||||||
|
|
||||||
* Revert src/constants/app.ts & change version to "-beta"
|
* `npx prettier --write ./sw_scripts/`
|
||||||
|
|
||||||
|
...to make sure the service worker scripts are in proper form. (It's only important if you changed something in that directory.)
|
||||||
|
|
||||||
* `cp sw_scripts/[ns]* dist/`
|
* `cp sw_scripts/[ns]* dist/`
|
||||||
|
|
||||||
... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
|
... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
|
||||||
|
|
||||||
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari`
|
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
|
||||||
|
|
||||||
|
* Revert src/constants/app.ts and package.json (if that was prod), edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -110,10 +112,12 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
|
|||||||
|
|
||||||
### Clear/Reset data & restart
|
### Clear/Reset data & restart
|
||||||
|
|
||||||
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data".)
|
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.)
|
||||||
* Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`).
|
* Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".)
|
||||||
* Clear notification permission (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications").
|
* Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.)
|
||||||
* Clear Cache Storage (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage).
|
* Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
|
||||||
|
|
||||||
|
(If you find more, add them to the HelpNotificationsView.vue file.)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -136,3 +140,4 @@ Gifts make the world go 'round!
|
|||||||
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
||||||
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
|
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
|
||||||
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
|
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
|
||||||
|
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
|
||||||
|
|||||||
35
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.1.6-beta",
|
"version": "0.2.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.1.6-beta",
|
"version": "0.2.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@tweenjs/tween.js": "^21.0.0",
|
"@tweenjs/tween.js": "^21.0.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@veramo/core": "^5.4.1",
|
"@veramo/core": "^5.4.1",
|
||||||
"@veramo/credential-w3c": "^5.4.1",
|
"@veramo/credential-w3c": "^5.4.1",
|
||||||
"@veramo/data-store": "^5.4.1",
|
"@veramo/data-store": "^5.4.1",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"git-describe": "^4.1.1",
|
"git-describe": "^4.1.1",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.2.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"localstorage-slim": "^2.5.0",
|
"localstorage-slim": "^2.5.0",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.4.3",
|
||||||
"merkletreejs": "^0.3.10",
|
"merkletreejs": "^0.3.10",
|
||||||
@@ -50,6 +52,7 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"three": "^0.156.1",
|
"three": "^0.156.1",
|
||||||
|
"ua-parser-js": "^1.0.37",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
@@ -62,6 +65,7 @@
|
|||||||
"@types/leaflet": "^1.9.4",
|
"@types/leaflet": "^1.9.4",
|
||||||
"@types/ramda": "^0.29.3",
|
"@types/ramda": "^0.29.3",
|
||||||
"@types/three": "^0.155.1",
|
"@types/three": "^0.155.1",
|
||||||
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||||
"@typescript-eslint/parser": "^6.6.0",
|
"@typescript-eslint/parser": "^6.6.0",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
@@ -8793,6 +8797,11 @@
|
|||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-yaml": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.13",
|
"version": "7.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
||||||
@@ -8975,6 +8984,12 @@
|
|||||||
"integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==",
|
"integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ua-parser-js": {
|
||||||
|
"version": "0.7.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
|
||||||
|
"integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/web-bluetooth": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "0.0.18",
|
"version": "0.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz",
|
||||||
@@ -11061,8 +11076,7 @@
|
|||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/array-buffer-byte-length": {
|
"node_modules/array-buffer-byte-length": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -18610,7 +18624,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
},
|
},
|
||||||
@@ -26834,9 +26847,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ua-parser-js": {
|
"node_modules/ua-parser-js": {
|
||||||
"version": "1.0.36",
|
"version": "1.0.37",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
|
||||||
"integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==",
|
"integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -26851,8 +26864,6 @@
|
|||||||
"url": "https://github.com/sponsors/faisalman"
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.1.6-beta",
|
"version": "0.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@tweenjs/tween.js": "^21.0.0",
|
"@tweenjs/tween.js": "^21.0.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@veramo/core": "^5.4.1",
|
"@veramo/core": "^5.4.1",
|
||||||
"@veramo/credential-w3c": "^5.4.1",
|
"@veramo/credential-w3c": "^5.4.1",
|
||||||
"@veramo/data-store": "^5.4.1",
|
"@veramo/data-store": "^5.4.1",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"git-describe": "^4.1.1",
|
"git-describe": "^4.1.1",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.2.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"localstorage-slim": "^2.5.0",
|
"localstorage-slim": "^2.5.0",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.4.3",
|
||||||
"merkletreejs": "^0.3.10",
|
"merkletreejs": "^0.3.10",
|
||||||
@@ -50,6 +52,7 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"three": "^0.156.1",
|
"three": "^0.156.1",
|
||||||
|
"ua-parser-js": "^1.0.37",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
@@ -62,6 +65,7 @@
|
|||||||
"@types/leaflet": "^1.9.4",
|
"@types/leaflet": "^1.9.4",
|
||||||
"@types/ramda": "^0.29.3",
|
"@types/ramda": "^0.29.3",
|
||||||
"@types/three": "^0.155.1",
|
"@types/three": "^0.155.1",
|
||||||
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||||
"@typescript-eslint/parser": "^6.6.0",
|
"@typescript-eslint/parser": "^6.6.0",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
|
|||||||
@@ -1,55 +1,28 @@
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
- 08 notifications :
|
|
||||||
- get it to work on Android
|
|
||||||
- get it to work on iOS
|
|
||||||
- lock down regenerate_vapid endpoint (so only we admins can do it on demand)
|
|
||||||
- make the app behave correctly when App Notifications are turned off
|
|
||||||
- remove "mute notifications"
|
|
||||||
- remove sleep in py-push-server app.py?
|
|
||||||
- see if we can detect OS-level notifications if turned off
|
|
||||||
- write troubleshooting docs for notifications
|
|
||||||
- make the "App Notifications" toggle on when they turn notifications on
|
|
||||||
- make the "App Notifications" toggle off when they turn notifications off
|
|
||||||
- in py-push-server, when sending a push to a subscriber and we get on a 410 "error #106", delete the subscription record
|
|
||||||
- https://gitea.anomalistdesign.com/trent_larson/py-push-server/pulls/3/files
|
|
||||||
- remove "notification push server" advanced setting since it only makes sense on the current domain
|
|
||||||
|
|
||||||
- .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
|
||||||
|
|
||||||
- .5 Add infinite scroll to gifts on the home page
|
|
||||||
|
|
||||||
- .5 If notifications are not enabled, add message to front page with link/button to enable
|
|
||||||
|
|
||||||
- fix notification error when first loading the app
|
|
||||||
- add note after contact addition that they can see your info
|
|
||||||
- enhance help page instructions for debugging
|
|
||||||
- add way to test quickly a push notification
|
|
||||||
- help instructions for PWA install problems (secret failed, must reinstall)
|
|
||||||
- look at other examples for better UI friend.tech
|
|
||||||
|
|
||||||
- show VC details... somehow:
|
- show VC details... somehow:
|
||||||
- 01 show my VCs - most interesting, or via search
|
- 01 show my VCs - most interesting, or via search
|
||||||
- 01 allow download of each VC (& confirmations, to show that they actually own their data)
|
- 04 allow user to download chains of VCs, mine + ones I can see about me from others
|
||||||
- 04 allow user to download VCs, mine + ones I can see about me from others
|
- add VC confirmation
|
||||||
- add VC confirmation?
|
|
||||||
|
|
||||||
- Release Minimum Viable Product :
|
|
||||||
- .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions)
|
|
||||||
- 08 thorough testing for errors & edge cases
|
|
||||||
- 01 ensure ability to recover server remotely, and add redundant access
|
|
||||||
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
|
||||||
- Add disclaimers.
|
|
||||||
- Switch default server to the public server.
|
|
||||||
- Deploy to a server.
|
|
||||||
- Ensure public server has limits that work for group adoption.
|
|
||||||
- Test PWA features on Android and iOS.
|
|
||||||
- Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts
|
|
||||||
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
|
||||||
|
|
||||||
|
- copy button for seed
|
||||||
|
- .5 If notifications are not enabled, add message to front page with link/button to enable
|
||||||
|
- record donations vs gives
|
||||||
|
- make server endpoint for full English description of limits
|
||||||
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
|
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
|
||||||
- allow some gives even if they aren't registered
|
- create a help-desk document & add screenshots
|
||||||
|
|
||||||
|
- 01 server - show all claim details when issued by the issuer
|
||||||
|
- 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list)
|
||||||
|
- bug - got error adding on Firefox user #0 as contact for themselves
|
||||||
|
- bug (that is hard to reproduce) - back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity
|
||||||
|
- bug (that is hard to reproduce) - in Chrome, install app then delete app and try from Chrome browser and see log errors "Uncaught TypeError: self.appendDailyLog is not a function"
|
||||||
|
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
|
||||||
|
- 01 send visibility signal as a VC and store it
|
||||||
|
- 04 remove 'rowid' references (that are sqlite-specific); may involve server
|
||||||
|
- 04 look at other examples for better UI friend.tech
|
||||||
|
- 01 make the prod build copy the sw_scripts
|
||||||
- .5 Add start date to project
|
- .5 Add start date to project
|
||||||
- .3 check that Android shows "back" buttons on screens without bottom tray
|
- .3 check that Android shows "back" buttons on screens without bottom tray
|
||||||
- .1 Make give description text box into something that expands as they type?
|
- .1 Make give description text box into something that expands as they type?
|
||||||
@@ -57,12 +30,13 @@ tasks:
|
|||||||
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
||||||
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
||||||
- .5 Display a more appealing confirmation on the map when erasing the marker
|
- .5 Display a more appealing confirmation on the map when erasing the marker
|
||||||
- .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch)
|
|
||||||
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
|
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
|
||||||
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
|
|
||||||
- switch some checks for activeDid to check for isRegistered
|
- switch some checks for activeDid to check for isRegistered
|
||||||
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||||
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
|
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
|
||||||
|
- warn if they're using the web (android only?)
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
|
||||||
|
https://web.dev/articles/get-installed-related-apps
|
||||||
|
|
||||||
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
|
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
|
||||||
|
|
||||||
@@ -70,6 +44,7 @@ tasks:
|
|||||||
- 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
|
||||||
- 01 parse input more robustly (with CSV lib and not commas)
|
- 01 parse input more robustly (with CSV lib and not commas)
|
||||||
|
|
||||||
- stats v1 :
|
- stats v1 :
|
||||||
- 01 show numeric stats
|
- 01 show numeric stats
|
||||||
- 04 show different graphic for projects vs people (gnome?) on world
|
- 04 show different graphic for projects vs people (gnome?) on world
|
||||||
@@ -85,6 +60,10 @@ tasks:
|
|||||||
- 24 Move to Vite
|
- 24 Move to Vite
|
||||||
- 32 accept images for projects
|
- 32 accept images for projects
|
||||||
- 32 accept images for contacts
|
- 32 accept images for contacts
|
||||||
|
- import project interactions from GitHub/GitLab and manage signing
|
||||||
|
|
||||||
|
- show total time offered to & fulfilled to a project
|
||||||
|
- show total time offered by & fulfilled by a contact
|
||||||
|
|
||||||
- linking between projects or plans :
|
- linking between projects or plans :
|
||||||
- show total time given to & from a project
|
- show total time given to & from a project
|
||||||
@@ -107,6 +86,7 @@ tasks:
|
|||||||
- automated tests, eg. cypress
|
- automated tests, eg. cypress
|
||||||
|
|
||||||
- Notifications (wake on the phone, push notifications)
|
- Notifications (wake on the phone, push notifications)
|
||||||
|
- 02 change push server so that the web-push/subscribe call sets up a thread for the 10-seconds-later push notification, but returns immediately to the callee
|
||||||
- pull instead of push, maybe via scheduled runs
|
- pull instead of push, maybe via scheduled runs
|
||||||
- have a notification pop-up on Mac screen
|
- have a notification pop-up on Mac screen
|
||||||
|
|
||||||
@@ -124,6 +104,8 @@ tasks:
|
|||||||
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
||||||
- 16 From the home screen, make the quick action even easier.
|
- 16 From the home screen, make the quick action even easier.
|
||||||
|
|
||||||
|
- allow some gives even if they aren't registered - maybe someday as a gift to the world, but we really want this to be built via personal connections
|
||||||
|
|
||||||
log:
|
log:
|
||||||
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
||||||
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27
|
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 799 B After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 37 KiB |
88
src/App.vue
@@ -156,11 +156,17 @@
|
|||||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
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">
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
<p class="text-lg mb-4">
|
<p v-if="serviceWorkerReady" class="text-lg mb-4">
|
||||||
Would you like to <b>turn on</b> notifications for this app?
|
Would you like to <b>turn on</b> notifications for this app?
|
||||||
</p>
|
</p>
|
||||||
|
<p v-else class="text-lg mb-4">
|
||||||
|
Waiting for system initialization, which may take up to 10
|
||||||
|
seconds...
|
||||||
|
<fa icon="spinner" spin />
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="serviceWorkerReady"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
@click="
|
@click="
|
||||||
close(notification.id);
|
close(notification.id);
|
||||||
@@ -169,6 +175,7 @@
|
|||||||
>
|
>
|
||||||
Turn on Notifications
|
Turn on Notifications
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
@@ -281,9 +288,10 @@ interface VapidResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
import { AppString } from "@/constants/app";
|
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||||
import { db } from "@/db/index";
|
import { db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { sendTestThroughPushServer } from "@/libs/util";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -297,12 +305,13 @@ export default class App extends Vue {
|
|||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
b64 = "";
|
b64 = "";
|
||||||
|
serviceWorkerReady = false;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
let pushUrl: string = AppString.DEFAULT_PUSH_SERVER;
|
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||||
if (settings?.webPushServer) {
|
if (settings?.webPushServer) {
|
||||||
pushUrl = settings.webPushServer;
|
pushUrl = settings.webPushServer;
|
||||||
}
|
}
|
||||||
@@ -328,17 +337,25 @@ export default class App extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Got an error initializing notifications:", error);
|
if (window.location.host.startsWith("localhost")) {
|
||||||
this.$notify(
|
console.log("Ignoring the error getting VAPID for local development.");
|
||||||
{
|
} else {
|
||||||
group: "alert",
|
console.error("Got an error initializing notifications:", error);
|
||||||
type: "danger",
|
this.$notify(
|
||||||
title: "Error Setting Notifications",
|
{
|
||||||
text: "Got an error setting notifications.",
|
group: "alert",
|
||||||
},
|
type: "danger",
|
||||||
-1,
|
title: "Error Setting Notifications",
|
||||||
);
|
text: "Got an error setting notifications.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// there may be a long pause here on first initialization
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
this.serviceWorkerReady = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessageToServiceWorker(
|
private sendMessageToServiceWorker(
|
||||||
@@ -366,6 +383,7 @@ export default class App extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private askPermission(): Promise<NotificationPermission> {
|
private askPermission(): Promise<NotificationPermission> {
|
||||||
|
console.log("Requesting permission for notifications:", navigator);
|
||||||
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
|
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
|
||||||
return Promise.reject("Service worker not available.");
|
return Promise.reject("Service worker not available.");
|
||||||
}
|
}
|
||||||
@@ -416,7 +434,7 @@ export default class App extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async turnOnNotifications() {
|
public async turnOnNotifications() {
|
||||||
return this.askPermission()
|
return this.askPermission()
|
||||||
.then((permission) => {
|
.then((permission) => {
|
||||||
console.log("Permission granted:", permission);
|
console.log("Permission granted:", permission);
|
||||||
@@ -430,15 +448,37 @@ export default class App extends Vue {
|
|||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
return registration.pushManager.getSubscription();
|
return registration.pushManager.getSubscription();
|
||||||
})
|
})
|
||||||
.then((subscription) => {
|
.then(async (subscription) => {
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
return this.sendSubscriptionToServer(subscription);
|
await this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Notification Setup Underway",
|
||||||
|
text: "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
this.sendSubscriptionToServer(subscription);
|
||||||
|
return subscription;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Subscription object is not available.");
|
throw new Error("Subscription object is not available.");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(async (subscription) => {
|
||||||
console.log("Subscription data sent to server.");
|
console.log(
|
||||||
|
"Subscription data sent to server and all finished successfully.",
|
||||||
|
);
|
||||||
|
await sendTestThroughPushServer(subscription, true);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Notifications Turned On",
|
||||||
|
text: "Notifications are on. You should see at least one on your device; if not, check the 'Troubleshoot' page.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -455,9 +495,7 @@ export default class App extends Vue {
|
|||||||
"An error occurred setting notification permissions:",
|
"An error occurred setting notification permissions:",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
alert(
|
alert("Some error occurred setting notification permissions.");
|
||||||
"Some error occurred setting notification permissions. See logs.",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,11 +542,7 @@ export default class App extends Vue {
|
|||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(
|
console.error("Push subscription failed:", error, options);
|
||||||
"Subscription or server communication failed:",
|
|
||||||
error,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Inform the user about the issue
|
// Inform the user about the issue
|
||||||
alert(
|
alert(
|
||||||
@@ -524,7 +558,7 @@ export default class App extends Vue {
|
|||||||
private sendSubscriptionToServer(
|
private sendSubscriptionToServer(
|
||||||
subscription: PushSubscription,
|
subscription: PushSubscription,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log("About to send subscription", subscription);
|
console.log("About to send subscription...", subscription);
|
||||||
return fetch("/web-push/subscribe", {
|
return fetch("/web-push/subscribe", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
BIN
src/assets/help/apple-icon.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
3
src/assets/help/apple-share-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" enable-background="new 0 0 50 50">
|
||||||
|
<path d="M30.3 13.7L25 8.4l-5.3 5.3-1.4-1.4L25 5.6l6.7 6.7z"/><path d="M24 7h2v21h-2z"/><path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 365 B |
BIN
src/assets/help/chrome-install-pwa.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
27
src/assets/help/creative-commons-circle.svg
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<circle fill="#FFFFFF" cx="37.785" cy="28.501" r="28.836"/>
|
||||||
|
<path d="M37.441-3.5c8.951,0,16.572,3.125,22.857,9.372c3.008,3.009,5.295,6.448,6.857,10.314
|
||||||
|
c1.561,3.867,2.344,7.971,2.344,12.314c0,4.381-0.773,8.486-2.314,12.313c-1.543,3.828-3.82,7.21-6.828,10.143
|
||||||
|
c-3.123,3.085-6.666,5.448-10.629,7.086c-3.961,1.638-8.057,2.457-12.285,2.457s-8.276-0.808-12.143-2.429
|
||||||
|
c-3.866-1.618-7.333-3.961-10.4-7.027c-3.067-3.066-5.4-6.524-7-10.372S5.5,32.767,5.5,28.5c0-4.229,0.809-8.295,2.428-12.2
|
||||||
|
c1.619-3.905,3.972-7.4,7.057-10.486C21.08-0.394,28.565-3.5,37.441-3.5z M37.557,2.272c-7.314,0-13.467,2.553-18.458,7.657
|
||||||
|
c-2.515,2.553-4.448,5.419-5.8,8.6c-1.354,3.181-2.029,6.505-2.029,9.972c0,3.429,0.675,6.734,2.029,9.913
|
||||||
|
c1.353,3.183,3.285,6.021,5.8,8.516c2.514,2.496,5.351,4.399,8.515,5.715c3.161,1.314,6.476,1.971,9.943,1.971
|
||||||
|
c3.428,0,6.75-0.665,9.973-1.999c3.219-1.335,6.121-3.257,8.713-5.771c4.99-4.876,7.484-10.99,7.484-18.344
|
||||||
|
c0-3.543-0.648-6.895-1.943-10.057c-1.293-3.162-3.18-5.98-5.654-8.458C50.984,4.844,44.795,2.272,37.557,2.272z M37.156,23.187
|
||||||
|
l-4.287,2.229c-0.458-0.951-1.019-1.619-1.685-2c-0.667-0.38-1.286-0.571-1.858-0.571c-2.856,0-4.286,1.885-4.286,5.657
|
||||||
|
c0,1.714,0.362,3.084,1.085,4.113c0.724,1.029,1.791,1.544,3.201,1.544c1.867,0,3.181-0.915,3.944-2.743l3.942,2
|
||||||
|
c-0.838,1.563-2,2.791-3.486,3.686c-1.484,0.896-3.123,1.343-4.914,1.343c-2.857,0-5.163-0.875-6.915-2.629
|
||||||
|
c-1.752-1.752-2.628-4.19-2.628-7.313c0-3.048,0.886-5.466,2.657-7.257c1.771-1.79,4.009-2.686,6.715-2.686
|
||||||
|
C32.604,18.558,35.441,20.101,37.156,23.187z M55.613,23.187l-4.229,2.229c-0.457-0.951-1.02-1.619-1.686-2
|
||||||
|
c-0.668-0.38-1.307-0.571-1.914-0.571c-2.857,0-4.287,1.885-4.287,5.657c0,1.714,0.363,3.084,1.086,4.113
|
||||||
|
c0.723,1.029,1.789,1.544,3.201,1.544c1.865,0,3.18-0.915,3.941-2.743l4,2c-0.875,1.563-2.057,2.791-3.541,3.686
|
||||||
|
c-1.486,0.896-3.105,1.343-4.857,1.343c-2.896,0-5.209-0.875-6.941-2.629c-1.736-1.752-2.602-4.19-2.602-7.313
|
||||||
|
c0-3.048,0.885-5.466,2.658-7.257c1.77-1.79,4.008-2.686,6.713-2.686C51.117,18.558,53.938,20.101,55.613,23.187z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
24
src/assets/help/creative-commons-zero.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="64px" height="64px" viewBox="-0.5 0.5 64 64" enable-background="new -0.5 0.5 64 64" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<circle fill="#FFFFFF" cx="31.325" cy="32.873" r="30.096"/>
|
||||||
|
<path id="text2809_1_" d="M31.5,14.08c-10.565,0-13.222,9.969-13.222,18.42c0,8.452,2.656,18.42,13.222,18.42
|
||||||
|
c10.564,0,13.221-9.968,13.221-18.42C44.721,24.049,42.064,14.08,31.5,14.08z M31.5,21.026c0.429,0,0.82,0.066,1.188,0.157
|
||||||
|
c0.761,0.656,1.133,1.561,0.403,2.823l-7.036,12.93c-0.216-1.636-0.247-3.24-0.247-4.437C25.808,28.777,26.066,21.026,31.5,21.026z
|
||||||
|
M36.766,26.987c0.373,1.984,0.426,4.056,0.426,5.513c0,3.723-0.258,11.475-5.69,11.475c-0.428,0-0.822-0.045-1.188-0.136
|
||||||
|
c-0.07-0.021-0.134-0.043-0.202-0.067c-0.112-0.032-0.23-0.068-0.336-0.11c-1.21-0.515-1.972-1.446-0.874-3.093L36.766,26.987z"/>
|
||||||
|
<path id="path2815_1_" d="M31.433,0.5c-8.877,0-16.359,3.09-22.454,9.3c-3.087,3.087-5.443,6.607-7.082,10.532
|
||||||
|
C0.297,24.219-0.5,28.271-0.5,32.5c0,4.268,0.797,8.32,2.397,12.168c1.6,3.85,3.921,7.312,6.969,10.396
|
||||||
|
c3.085,3.049,6.549,5.399,10.398,7.037c3.886,1.602,7.939,2.398,12.169,2.398c4.229,0,8.34-0.826,12.303-2.465
|
||||||
|
c3.962-1.639,7.496-3.994,10.621-7.081c3.011-2.933,5.289-6.297,6.812-10.106C62.73,41,63.5,36.883,63.5,32.5
|
||||||
|
c0-4.343-0.77-8.454-2.33-12.303c-1.562-3.885-3.848-7.32-6.857-10.33C48.025,3.619,40.385,0.5,31.433,0.5z M31.567,6.259
|
||||||
|
c7.238,0,13.412,2.566,18.554,7.709c2.477,2.477,4.375,5.31,5.67,8.471c1.296,3.162,1.949,6.518,1.949,10.061
|
||||||
|
c0,7.354-2.516,13.454-7.506,18.33c-2.592,2.516-5.502,4.447-8.74,5.781c-3.2,1.334-6.498,1.994-9.927,1.994
|
||||||
|
c-3.468,0-6.788-0.653-9.949-1.948c-3.163-1.334-6.001-3.238-8.516-5.716c-2.515-2.514-4.455-5.353-5.826-8.516
|
||||||
|
c-1.333-3.199-2.017-6.498-2.017-9.927c0-3.467,0.684-6.787,2.017-9.949c1.371-3.2,3.312-6.074,5.826-8.628
|
||||||
|
C18.092,8.818,24.252,6.259,31.567,6.259z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/help/install-android-chrome.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/help/mac-installed-app-settings.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
src/assets/help/windows-system-enable-notifications.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -123,9 +123,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text: err.message || "There was an error retrieving your settings.",
|
||||||
err.message ||
|
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -105,9 +105,7 @@ export default class OfferDialog extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text: err.message || "There was an error retrieving your settings.",
|
||||||
err.message ||
|
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<!-- QUICK NAV -->
|
||||||
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
||||||
<ul class="flex text-2xl p-2 gap-2">
|
<ul class="flex text-2xl p-2 gap-2 max-w-3xl mx-auto">
|
||||||
<!-- Home Feed -->
|
<!-- Home Feed -->
|
||||||
<li
|
<li
|
||||||
:class="{
|
:class="{
|
||||||
|
|||||||
58
src/components/TopMessage.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-center text-red-500">{{ message }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { AppString } from "@/constants/app";
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class TopMessage extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
@Prop selected = "";
|
||||||
|
|
||||||
|
message = "";
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
if (
|
||||||
|
settings?.warnIfTestServer &&
|
||||||
|
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||||
|
) {
|
||||||
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
|
this.message = "You're linked to a non-prod server, user " + didPrefix;
|
||||||
|
} else if (
|
||||||
|
settings?.warnIfProdServer &&
|
||||||
|
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||||
|
) {
|
||||||
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
|
this.message =
|
||||||
|
"You're linked to the production server, user " + didPrefix;
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Detecting Server",
|
||||||
|
text: JSON.stringify(err),
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -4,21 +4,20 @@
|
|||||||
* See also ../libs/veramo/setup.ts
|
* See also ../libs/veramo/setup.ts
|
||||||
*/
|
*/
|
||||||
export enum AppString {
|
export enum AppString {
|
||||||
APP_NAME = "Time Safari",
|
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||||
|
|
||||||
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
|
||||||
|
|
||||||
PROD_PUSH_SERVER = "https://timesafari.app",
|
PROD_PUSH_SERVER = "https://timesafari.app",
|
||||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
||||||
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
||||||
|
|
||||||
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
|
||||||
|
|
||||||
|
export const DEFAULT_PUSH_SERVER =
|
||||||
|
window.location.protocol + "//" + window.location.host;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible values for "group" and "type" are in App.vue.
|
* The possible values for "group" and "type" are in App.vue.
|
||||||
* From the notiwind package
|
* From the notiwind package
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import BaseDexie, { Table } from "dexie";
|
import BaseDexie, { Table } from "dexie";
|
||||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||||
import { Account, AccountsSchema } from "./tables/accounts";
|
import { Account, AccountsSchema } from "./tables/accounts";
|
||||||
import { Contact, ContactsSchema } from "./tables/contacts";
|
import { Contact, ContactSchema } from "./tables/contacts";
|
||||||
|
import { Log, LogSchema } from "./tables/logs";
|
||||||
import {
|
import {
|
||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
Settings,
|
Settings,
|
||||||
SettingsSchema,
|
SettingsSchema,
|
||||||
} from "./tables/settings";
|
} from "./tables/settings";
|
||||||
import { AppString } from "@/constants/app";
|
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||||
|
|
||||||
// Define types for tables that hold sensitive and non-sensitive data
|
// Define types for tables that hold sensitive and non-sensitive data
|
||||||
type SensitiveTables = { accounts: Table<Account> };
|
type SensitiveTables = { accounts: Table<Account> };
|
||||||
type NonsensitiveTables = {
|
type NonsensitiveTables = {
|
||||||
contacts: Table<Contact>;
|
contacts: Table<Contact>;
|
||||||
|
logs: Table<Log>;
|
||||||
settings: Table<Settings>;
|
settings: Table<Settings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,7 +28,11 @@ export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
|||||||
const SensitiveSchemas = { ...AccountsSchema };
|
const SensitiveSchemas = { ...AccountsSchema };
|
||||||
|
|
||||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema };
|
const NonsensitiveSchemas = {
|
||||||
|
...ContactSchema,
|
||||||
|
...LogSchema,
|
||||||
|
...SettingsSchema,
|
||||||
|
};
|
||||||
|
|
||||||
// Manage the encryption key. If not present in localStorage, create and store it.
|
// Manage the encryption key. If not present in localStorage, create and store it.
|
||||||
const secret =
|
const secret =
|
||||||
@@ -38,15 +44,14 @@ encrypted(accountsDB, { secretKey: secret });
|
|||||||
|
|
||||||
// Define the schema for our databases
|
// Define the schema for our databases
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
db.version(1).stores(NonsensitiveSchemas);
|
// v1 was contacts & settings
|
||||||
|
// v2 added logs
|
||||||
|
db.version(2).stores(NonsensitiveSchemas);
|
||||||
|
|
||||||
// Event handler to initialize the non-sensitive database with default settings
|
// Event handler to initialize the non-sensitive database with default settings
|
||||||
db.on("populate", () => {
|
db.on("populate", () => {
|
||||||
db.settings.add({
|
db.settings.add({
|
||||||
id: MASTER_SETTINGS_KEY,
|
id: MASTER_SETTINGS_KEY,
|
||||||
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
||||||
|
|
||||||
// remember that things you add from now on aren't automatically in the DB for old users
|
|
||||||
webPushServer: AppString.DEFAULT_PUSH_SERVER,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
export interface Contact {
|
export interface Contact {
|
||||||
did: string;
|
did: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||||
publicKeyBase64?: string;
|
publicKeyBase64?: string;
|
||||||
seesMe?: boolean;
|
seesMe?: boolean;
|
||||||
registered?: boolean;
|
registered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactsSchema = {
|
export const ContactSchema = {
|
||||||
contacts: "&did, name, publicKeyBase64, registered, seesMe",
|
contacts: "&did, name", // no need to key by other things
|
||||||
};
|
};
|
||||||
|
|||||||
11
src/db/tables/logs.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface Log {
|
||||||
|
date: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogSchema = {
|
||||||
|
// Currently keyed by "date" because A) today's log data is what we need so we append, and
|
||||||
|
// B) we don't want it to grow so we remove everything if this is the first entry today.
|
||||||
|
// See safari-notifications.js logMessage for the associated logic.
|
||||||
|
logs: "date", // definitely don't key by the potentially large message field
|
||||||
|
};
|
||||||
@@ -12,15 +12,17 @@ export type BoundingBox = {
|
|||||||
* Settings type encompasses user-specific configuration details.
|
* Settings type encompasses user-specific configuration details.
|
||||||
*/
|
*/
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
id: number; // Only one entry using MASTER_SETTINGS_KEY
|
id: number; // Only one entry, keyed with MASTER_SETTINGS_KEY
|
||||||
|
|
||||||
activeDid?: string; // Active Decentralized ID
|
activeDid?: string; // Active Decentralized ID
|
||||||
apiServer?: string; // API server URL
|
apiServer?: string; // API server URL
|
||||||
firstName?: string; // User's first name
|
firstName?: string; // User's first name
|
||||||
lastName?: string; // deprecated - put all names in firstName
|
|
||||||
lastViewedClaimId?: string; // Last viewed claim ID
|
|
||||||
lastNotifiedClaimId?: string; // Last notified claim ID
|
|
||||||
isRegistered?: boolean;
|
isRegistered?: boolean;
|
||||||
webPushServer?: string; // Web Push server URL
|
lastName?: string; // deprecated - put all names in firstName
|
||||||
|
lastNotifiedClaimId?: string; // Last notified claim ID
|
||||||
|
lastViewedClaimId?: string; // Last viewed claim ID
|
||||||
|
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
||||||
|
reminderOn?: boolean; // Toggle to enable or disable reminders
|
||||||
|
|
||||||
// Array of named search boxes defined by bounding boxes
|
// Array of named search boxes defined by bounding boxes
|
||||||
searchBoxes?: Array<{
|
searchBoxes?: Array<{
|
||||||
@@ -30,8 +32,9 @@ export type Settings = {
|
|||||||
|
|
||||||
showContactGivesInline?: boolean; // Display contact inline or not
|
showContactGivesInline?: boolean; // Display contact inline or not
|
||||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
||||||
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
warnIfProdServer?: boolean; // Warn if using a production server
|
||||||
reminderOn?: boolean; // Toggle to enable or disable reminders
|
warnIfTestServer?: boolean; // Warn if using a testing server
|
||||||
|
webPushServer?: string; // Web Push server URL
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -173,3 +173,19 @@ export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
|
|||||||
|
|
||||||
return jwt.payload;
|
return jwt.payload;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const nextDerivationPath = (origDerivPath: string) => {
|
||||||
|
let lastStr = origDerivPath.split("/").slice(-1)[0];
|
||||||
|
if (lastStr.endsWith("'")) {
|
||||||
|
lastStr = lastStr.slice(0, -1);
|
||||||
|
}
|
||||||
|
const lastNum = parseInt(lastStr, 10);
|
||||||
|
const newLastNum = lastNum + 1;
|
||||||
|
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : "");
|
||||||
|
const newDerivPath = origDerivPath
|
||||||
|
.split("/")
|
||||||
|
.slice(0, -1)
|
||||||
|
.concat([newLastStr])
|
||||||
|
.join("/");
|
||||||
|
return newDerivPath;
|
||||||
|
};
|
||||||
|
|||||||
@@ -486,6 +486,10 @@ export interface ProjectData {
|
|||||||
* URL referencing information about the project
|
* URL referencing information about the project
|
||||||
**/
|
**/
|
||||||
handleId: string;
|
handleId: string;
|
||||||
|
/**
|
||||||
|
* The DID of the issuer
|
||||||
|
*/
|
||||||
|
issuerDid: string;
|
||||||
/**
|
/**
|
||||||
* The Identier of the project
|
* The Identier of the project
|
||||||
**/
|
**/
|
||||||
|
|||||||
@@ -1,5 +1,74 @@
|
|||||||
// many of these are also found in endorser-mobile utility.ts
|
// many of these are also found in endorser-mobile utility.ts
|
||||||
|
|
||||||
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
|
||||||
export const isGlobalUri = (uri: string) => {
|
export const isGlobalUri = (uri: string) => {
|
||||||
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, too),
|
||||||
|
// and make sure they can take all actions while the notification shows.
|
||||||
|
export const ONBOARD_MESSAGE =
|
||||||
|
"1) Check that they've entered their name. 2) Go to the scanning page: use the Contacts page and click on the QR icon at the top, and then scan and register them. 3) Have them go to that page and scan you.";
|
||||||
|
|
||||||
|
export const sendTestThroughPushServer = async (
|
||||||
|
subscription: PushSubscription,
|
||||||
|
skipFilter: boolean,
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
||||||
|
if (settings?.webPushServer) {
|
||||||
|
pushUrl = settings.webPushServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
|
||||||
|
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
|
||||||
|
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
|
||||||
|
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
|
||||||
|
|
||||||
|
const auth = Buffer.from(subscription.getKey("auth"));
|
||||||
|
const authB64 = auth
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
const p256dh = Buffer.from(subscription.getKey("p256dh"));
|
||||||
|
const p256dhB64 = p256dh
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
const newPayload = {
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
keys: {
|
||||||
|
auth: authB64,
|
||||||
|
p256dh: p256dhB64,
|
||||||
|
},
|
||||||
|
message: `Test, where you will see this message ${
|
||||||
|
skipFilter ? "un" : ""
|
||||||
|
}filtered.`,
|
||||||
|
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
|
||||||
|
};
|
||||||
|
console.log("Sending a test web push message:", newPayload);
|
||||||
|
const payloadStr = JSON.stringify(newPayload);
|
||||||
|
const response = await axios.post(
|
||||||
|
pushUrl + "/web-push/send-test",
|
||||||
|
payloadStr,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Got response from web push server:", response);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
|||||||
import {
|
import {
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
faBan,
|
faBan,
|
||||||
faBitcoinSign,
|
faBitcoinSign,
|
||||||
faBurst,
|
faBurst,
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
faSpinner,
|
faSpinner,
|
||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
|
faSquarePlus,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
@@ -65,6 +67,7 @@ import {
|
|||||||
library.add(
|
library.add(
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
faBan,
|
faBan,
|
||||||
faBitcoinSign,
|
faBitcoinSign,
|
||||||
faBurst,
|
faBurst,
|
||||||
@@ -107,6 +110,7 @@ library.add(
|
|||||||
faSpinner,
|
faSpinner,
|
||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
|
faSquarePlus,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
||||||
Your Identity
|
Your Identity
|
||||||
@@ -32,8 +34,24 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ID notice -->
|
||||||
|
<div
|
||||||
|
v-if="!activeDid"
|
||||||
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
||||||
|
>
|
||||||
|
<p class="mb-4">
|
||||||
|
<b>Note:</b> Before you can take any action, you need an ID.
|
||||||
|
</p>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'start' }"
|
||||||
|
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Generate Identity
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Registration notice -->
|
<!-- Registration notice -->
|
||||||
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
|
<!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
|
||||||
<div
|
<div
|
||||||
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
|
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
|
||||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
||||||
@@ -54,11 +72,14 @@
|
|||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
||||||
{{ givenName }}
|
{{ givenName }}
|
||||||
|
<router-link :to="{ name: 'new-edit-account' }">
|
||||||
|
<fa icon="pen" class="text-xs text-blue-500 mb-1"></fa>
|
||||||
|
</router-link>
|
||||||
</h2>
|
</h2>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'new-edit-account' }"
|
:to="{ name: 'new-edit-account' }"
|
||||||
class="block w-full text-center text-md text-slate-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
(Set Your Name)
|
(Set Your Name)
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -79,34 +100,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'new-edit-account' }"
|
|
||||||
class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
|
|
||||||
>
|
|
||||||
Edit Identity
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||||
<label
|
<div
|
||||||
for="toggleNotifications"
|
v-if="!notificationMaybeChanged"
|
||||||
class="flex items-center justify-between cursor-pointer"
|
class="flex items-center justify-between cursor-pointer"
|
||||||
@click="
|
@click="showNotificationChoice()"
|
||||||
!toggleNotifications
|
|
||||||
? this.$notify(
|
|
||||||
{
|
|
||||||
group: 'modal',
|
|
||||||
type: 'notification-permission',
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
: this.$notify(
|
|
||||||
{
|
|
||||||
group: 'modal',
|
|
||||||
type: 'notification-off',
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<div>App Notifications</div>
|
<div>App Notifications</div>
|
||||||
@@ -115,8 +113,8 @@
|
|||||||
<!-- input -->
|
<!-- input -->
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="toggleNotifications"
|
v-model="isSubscribed"
|
||||||
name="toggleNotifications"
|
name="toggleNotificationsInput"
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
/>
|
/>
|
||||||
<!-- line -->
|
<!-- line -->
|
||||||
@@ -126,72 +124,56 @@
|
|||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
<label
|
<div v-else>
|
||||||
for="toggleMuteNotifications"
|
Notification status may have changed. Refresh this page to see the
|
||||||
class="flex items-center justify-between cursor-pointer mt-4"
|
latest setting.
|
||||||
@click="
|
</div>
|
||||||
this.$notify(
|
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
|
||||||
{
|
Troubleshoot your notification setup.
|
||||||
group: 'modal',
|
</router-link>
|
||||||
type: 'notification-mute',
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<!-- label -->
|
|
||||||
<div>Mute Notifications</div>
|
|
||||||
<!-- toggle -->
|
|
||||||
<div class="relative ml-2">
|
|
||||||
<!-- input -->
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="toggleMuteNotifications"
|
|
||||||
class="sr-only"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
<!-- line -->
|
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
|
||||||
<!-- dot -->
|
|
||||||
<div
|
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Data Export</h3>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'seed-backup' }"
|
:to="{ name: 'seed-backup' }"
|
||||||
|
v-if="activeDid"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
Backup Identifier Seed
|
Backup Identifier Seed
|
||||||
</router-link>
|
</router-link>
|
||||||
<a
|
|
||||||
|
<button
|
||||||
|
v-bind:class="computedStartDownloadLinkClassNames()"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
||||||
@click="exportDatabase()"
|
@click="exportDatabase()"
|
||||||
>
|
>
|
||||||
Download Settings & Contacts
|
Download Settings & Contacts
|
||||||
<br />
|
<br />
|
||||||
(excluding Identifier Data)
|
(excluding Identifier Data)
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
ref="downloadLink"
|
||||||
|
v-bind:class="computedDownloadLinkClassNames()"
|
||||||
|
class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
|
||||||
|
>
|
||||||
|
If no download happened yet, click again here to download now.
|
||||||
</a>
|
</a>
|
||||||
<a ref="downloadLink" />
|
|
||||||
|
|
||||||
<div v-if="activeDid" class="my-8">
|
<div v-if="activeDid" class="flex mt-8 py-2">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Rate Limits</h3>
|
<h3 class="text-sm uppercase font-semibold">Rate Limits</h3>
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md ml-2 mr-2 mb-2"
|
||||||
@click="checkLimits()"
|
@click="checkLimits()"
|
||||||
>
|
>
|
||||||
Check Limits
|
Check Limits
|
||||||
</button>
|
</button>
|
||||||
<!-- show spinner if loading limits -->
|
<!-- show spinner if loading limits -->
|
||||||
<div v-if="loadingLimits" class="text-center mb-4">
|
<div v-if="loadingLimits" class="text-center">
|
||||||
Checking… <fa icon="spinner" class="fa-spin"></fa>
|
Checking… <fa icon="spinner" class="fa-spin"></fa>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div>
|
||||||
{{ limitsMessage }}
|
{{ limitsMessage }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!!limits?.nextWeekBeginDateTime">
|
<div v-if="!!limits?.nextWeekBeginDateTime">
|
||||||
@@ -230,7 +212,7 @@
|
|||||||
<div v-if="showAdvanced">
|
<div v-if="showAdvanced">
|
||||||
<p class="text-rose-600 mb-8">
|
<p class="text-rose-600 mb-8">
|
||||||
Beware: the features here can be confusing and even change data in ways
|
Beware: the features here can be confusing and even change data in ways
|
||||||
you do not expect. But we support your freedoms!
|
you do not expect. But we support your freedom!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Deep Identity Details -->
|
<!-- Deep Identity Details -->
|
||||||
@@ -315,29 +297,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<router-link
|
<div class="grid-cols-2 mb-4">
|
||||||
:to="{ name: 'statistics' }"
|
<span class="text-slate-500 text-sm font-bold mb-2">Data Import</span>
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
<input type="file" @change="uploadFile" class="ml-2" />
|
||||||
>
|
<div v-if="showContactImport()">
|
||||||
See Global Animated History of Giving
|
<button
|
||||||
</router-link>
|
class="block text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
|
||||||
|
@click="submitFile()"
|
||||||
|
>
|
||||||
|
Import Settings & Contacts
|
||||||
|
<br />
|
||||||
|
(excluding Identifier Data)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex py-2">
|
||||||
|
<button>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'statistics' }"
|
||||||
|
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
See Global Animated History of Giving
|
||||||
|
</router-link>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
<!-- id used by puppeteer test script -->
|
||||||
<router-link
|
<router-link
|
||||||
id="switch-identity-link"
|
id="switch-identity-link"
|
||||||
:to="{ name: 'identity-switcher' }"
|
:to="{ name: 'identity-switcher' }"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
class="block w-fit 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
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<button
|
|
||||||
@click="alertWebPushSubscription()"
|
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
|
||||||
>
|
|
||||||
Show Subscription from Web Push Server
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex py-4">
|
<div class="flex py-4">
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
||||||
<input
|
<input
|
||||||
@@ -372,6 +366,46 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label
|
||||||
|
for="toggleProdWarningMessage"
|
||||||
|
class="flex items-center justify-between cursor-pointer my-4"
|
||||||
|
@click="toggleProdWarning"
|
||||||
|
>
|
||||||
|
<!-- label -->
|
||||||
|
<h2>Show warning if on prod server</h2>
|
||||||
|
<!-- toggle -->
|
||||||
|
<div class="relative ml-2">
|
||||||
|
<!-- input -->
|
||||||
|
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
|
||||||
|
<!-- line -->
|
||||||
|
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||||
|
<!-- dot -->
|
||||||
|
<div
|
||||||
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
for="toggleTestWarningMessage"
|
||||||
|
class="flex items-center justify-between cursor-pointer my-4"
|
||||||
|
@click="toggleTestWarning"
|
||||||
|
>
|
||||||
|
<!-- label -->
|
||||||
|
<h2>Show warning if on non-prod server</h2>
|
||||||
|
<!-- toggle -->
|
||||||
|
<div class="relative ml-2">
|
||||||
|
<!-- input -->
|
||||||
|
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
|
||||||
|
<!-- line -->
|
||||||
|
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||||
|
<!-- dot -->
|
||||||
|
<div
|
||||||
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="flex py-4">
|
<div class="flex py-4">
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||||
Notification Push Server
|
Notification Push Server
|
||||||
@@ -418,17 +452,21 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError, AxiosRequestConfig } from "axios";
|
import { AxiosError, AxiosRequestConfig } from "axios";
|
||||||
|
import Dexie from "dexie";
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
||||||
|
import { ImportProgress } from "dexie-export-import/dist/import";
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -447,7 +485,9 @@ interface IAccount {
|
|||||||
derivationPath: string;
|
derivationPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
const inputFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
|
@Component({ components: { QuickNav, TopMessage } })
|
||||||
export default class AccountViewView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
@@ -457,8 +497,11 @@ export default class AccountViewView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
apiServerInput = "";
|
apiServerInput = "";
|
||||||
derivationPath = "";
|
derivationPath = "";
|
||||||
|
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
|
||||||
givenName = "";
|
givenName = "";
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
|
isSubscribed = false;
|
||||||
|
notificationMaybeChanged = false;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
publicBase64 = "";
|
publicBase64 = "";
|
||||||
@@ -477,39 +520,90 @@ export default class AccountViewView extends Vue {
|
|||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
|
|
||||||
subscription: PushSubscription | null = null;
|
subscription: PushSubscription | null = null;
|
||||||
|
warnIfProdServer = false;
|
||||||
|
warnIfTestServer = false;
|
||||||
|
|
||||||
private isSubscribed = false;
|
async beforeCreate() {
|
||||||
get toggleNotifications() {
|
await accountsDB.open();
|
||||||
return this.isSubscribed;
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
}
|
}
|
||||||
set toggleNotifications(value) {
|
|
||||||
this.isSubscribed = value;
|
/**
|
||||||
|
* Async function executed when the component is created.
|
||||||
|
* Initializes the component's state with values from the database,
|
||||||
|
* handles identity-related tasks, and checks limitations.
|
||||||
|
*
|
||||||
|
* @throws Will display specific messages to the user based on different errors.
|
||||||
|
*/
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
|
||||||
|
// Initialize component state with values from the database or defaults
|
||||||
|
this.initializeState(settings);
|
||||||
|
|
||||||
|
// Get and process the identity
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
if (identity) {
|
||||||
|
this.processIdentity(identity);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.handleError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
this.subscription = await registration.pushManager.getSubscription();
|
||||||
|
this.isSubscribed = !!this.subscription;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Mount error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.downloadUrl) {
|
||||||
|
URL.revokeObjectURL(this.downloadUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes component state with values from the database or defaults.
|
||||||
|
* @param {SettingsType} settings - Object containing settings from the database.
|
||||||
|
*/
|
||||||
|
initializeState(settings: Settings | undefined) {
|
||||||
|
this.activeDid = (settings?.activeDid as string) || "";
|
||||||
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
|
this.apiServerInput = (settings?.apiServer as string) || "";
|
||||||
|
this.givenName =
|
||||||
|
(settings?.firstName || "") +
|
||||||
|
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
||||||
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
this.warnIfProdServer = !!settings?.warnIfProdServer;
|
||||||
|
this.warnIfTestServer = !!settings?.warnIfTestServer;
|
||||||
|
this.webPushServer = (settings?.webPushServer as string) || "";
|
||||||
|
this.webPushServerInput = (settings?.webPushServer as string) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
||||||
try {
|
try {
|
||||||
// Open the accounts database
|
// Open the accounts database
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to open accounts database:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let account: { identity?: string } | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Search for the account with the matching DID (decentralized identifier)
|
// Search for the account with the matching DID (decentralized identifier)
|
||||||
account = await accountsDB.accounts
|
const account: { identity?: string } | undefined =
|
||||||
.where("did")
|
await accountsDB.accounts.where("did").equals(activeDid).first();
|
||||||
.equals(activeDid)
|
|
||||||
.first();
|
// Return parsed identity or null if not found
|
||||||
|
return JSON.parse((account?.identity as string) || "null");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to find account:", error);
|
console.error("Failed to find account:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return parsed identity or null if not found
|
|
||||||
return JSON.parse((account?.identity as string) || "null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -551,69 +645,20 @@ export default class AccountViewView extends Vue {
|
|||||||
this.updateShowContactAmounts();
|
this.updateShowContactAmounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleProdWarning() {
|
||||||
|
this.warnIfProdServer = !this.warnIfProdServer;
|
||||||
|
this.updateWarnIfProdServer(this.warnIfProdServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTestWarning() {
|
||||||
|
this.warnIfTestServer = !this.warnIfTestServer;
|
||||||
|
this.updateWarnIfTestServer(this.warnIfTestServer);
|
||||||
|
}
|
||||||
|
|
||||||
readableTime(timeStr: string) {
|
readableTime(timeStr: string) {
|
||||||
return timeStr.substring(0, timeStr.indexOf("T"));
|
return timeStr.substring(0, timeStr.indexOf("T"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async beforeCreate() {
|
|
||||||
await accountsDB.open();
|
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Async function executed when the component is created.
|
|
||||||
* Initializes the component's state with values from the database,
|
|
||||||
* handles identity-related tasks, and checks limitations.
|
|
||||||
*
|
|
||||||
* @throws Will display specific messages to the user based on different errors.
|
|
||||||
*/
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
|
|
||||||
// Initialize component state with values from the database or defaults
|
|
||||||
this.initializeState(settings);
|
|
||||||
|
|
||||||
// Get and process the identity
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
|
||||||
if (identity) {
|
|
||||||
this.processIdentity(identity);
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
this.handleError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
try {
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
|
||||||
this.subscription = await registration.pushManager.getSubscription();
|
|
||||||
this.toggleNotifications = !!this.subscription;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Mount error:", error);
|
|
||||||
this.toggleNotifications = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes component state with values from the database or defaults.
|
|
||||||
* @param {SettingsType} settings - Object containing settings from the database.
|
|
||||||
*/
|
|
||||||
initializeState(settings: Settings | undefined) {
|
|
||||||
this.activeDid = (settings?.activeDid as string) || "";
|
|
||||||
this.apiServer = (settings?.apiServer as string) || "";
|
|
||||||
this.apiServerInput = (settings?.apiServer as string) || "";
|
|
||||||
this.givenName =
|
|
||||||
(settings?.firstName || "") +
|
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
|
||||||
this.webPushServer = (settings?.webPushServer as string) || "";
|
|
||||||
this.webPushServerInput = (settings?.webPushServer as string) || "";
|
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the identity and updates the component's state.
|
* Processes the identity and updates the component's state.
|
||||||
* @param {IdentityType} identity - Object containing identity information.
|
* @param {IdentityType} identity - Object containing identity information.
|
||||||
@@ -638,6 +683,31 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showNotificationChoice() {
|
||||||
|
if (!this.subscription) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "notification-permission",
|
||||||
|
title: "", // unused, only here to satisfy type check
|
||||||
|
text: "", // unused, only here to satisfy type check
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "notification-off",
|
||||||
|
title: "", // unused, only here to satisfy type check
|
||||||
|
text: "", // unused, only here to satisfy type check
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.notificationMaybeChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles errors and updates the component's state accordingly.
|
* Handles errors and updates the component's state accordingly.
|
||||||
* @param {Error} err - The error object.
|
* @param {Error} err - The error object.
|
||||||
@@ -676,12 +746,58 @@ export default class AccountViewView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Updating Contact Setting",
|
title: "Error Updating Contact Setting",
|
||||||
text: "Clear your cache and start over (after data backup).",
|
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Telling user to clear cache after contact setting update because:",
|
"Telling user to try again after contact setting update because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateWarnIfProdServer(newSetting: boolean) {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
warnIfProdServer: newSetting,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Prod Warning",
|
||||||
|
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to try again after setting update because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateWarnIfTestServer(newSetting: boolean) {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
warnIfTestServer: newSetting,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Test Warning",
|
||||||
|
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to try again after setting update because:",
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -698,13 +814,13 @@ export default class AccountViewView extends Vue {
|
|||||||
const blob = await this.generateDatabaseBlob();
|
const blob = await this.generateDatabaseBlob();
|
||||||
|
|
||||||
// Create a temporary URL for the blob
|
// Create a temporary URL for the blob
|
||||||
const url = this.createBlobURL(blob);
|
this.downloadUrl = this.createBlobURL(blob);
|
||||||
|
|
||||||
// Trigger the download
|
// Trigger the download
|
||||||
this.downloadDatabaseBackup(url);
|
this.downloadDatabaseBackup(this.downloadUrl);
|
||||||
|
|
||||||
// Revoke the temporary URL
|
// Revoke the temporary URL -- not yet because of DuckDuckGo download failure
|
||||||
URL.revokeObjectURL(url);
|
//URL.revokeObjectURL(this.downloadUrl);
|
||||||
|
|
||||||
// Notify the user that the download has started
|
// Notify the user that the download has started
|
||||||
this.notifyDownloadStarted();
|
this.notifyDownloadStarted();
|
||||||
@@ -741,7 +857,19 @@ export default class AccountViewView extends Vue {
|
|||||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||||
downloadAnchor.href = url;
|
downloadAnchor.href = url;
|
||||||
downloadAnchor.download = `${db.name}-backup.json`;
|
downloadAnchor.download = `${db.name}-backup.json`;
|
||||||
downloadAnchor.click();
|
downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedStartDownloadLinkClassNames() {
|
||||||
|
return {
|
||||||
|
invisible: this.downloadUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedDownloadLinkClassNames() {
|
||||||
|
return {
|
||||||
|
invisible: !this.downloadUrl,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -753,7 +881,7 @@ export default class AccountViewView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Download Started",
|
title: "Download Started",
|
||||||
text: "See your downloads directory for the backup.",
|
text: "See your downloads directory for the backup. It is in the Dexie format.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -777,6 +905,43 @@ export default class AccountViewView extends Vue {
|
|||||||
console.error("Export Error:", error);
|
console.error("Export Error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
async uploadFile(event: any) {
|
||||||
|
inputFileNameRef.value = event.target.files[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
showContactImport() {
|
||||||
|
return !!inputFileNameRef.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously imports the database from a downloadable JSON file.
|
||||||
|
*
|
||||||
|
* @throws Will notify the user if there is an export error.
|
||||||
|
*/
|
||||||
|
async submitFile() {
|
||||||
|
if (inputFileNameRef.value != null) {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"This will replace all settings and contacts, so we recommend you first do the backup step above." +
|
||||||
|
" Are you sure you want to import and replace all contacts and settings?",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await db.delete();
|
||||||
|
await Dexie.import(inputFileNameRef.value, {
|
||||||
|
progressCallback: this.progressCallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private progressCallback(progress: ImportProgress) {
|
||||||
|
console.log(
|
||||||
|
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async checkLimits() {
|
async checkLimits() {
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
if (identity) {
|
if (identity) {
|
||||||
@@ -906,6 +1071,7 @@ export default class AccountViewView extends Vue {
|
|||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = accounts[accountNum - 1];
|
const account = accounts[accountNum - 1];
|
||||||
|
|
||||||
|
await db.open();
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
|
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
|
||||||
|
|
||||||
this.updateActiveAccountProperties(account);
|
this.updateActiveAccountProperties(account);
|
||||||
@@ -954,13 +1120,5 @@ export default class AccountViewView extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
alertWebPushSubscription() {
|
|
||||||
console.log(
|
|
||||||
"Web push subscription:",
|
|
||||||
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
|
|
||||||
);
|
|
||||||
alert(JSON.stringify(this.subscription));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav />
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -19,9 +19,9 @@
|
|||||||
<!-- Details -->
|
<!-- Details -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="block pb-4 flex gap-4 overflow-hidden">
|
<div class="block flex gap-4 overflow-hidden">
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<h2 class="text-xl">{{ veriClaim.id }}</h2>
|
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div>
|
<div>
|
||||||
{{ veriClaim.claimType }}
|
{{ veriClaim.claimType }}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-bold text-2xl">Confirmations</h2>
|
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
|
||||||
|
|
||||||
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
|
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
|
||||||
<span v-else-if="totalConfirmers() === 1">
|
<span v-else-if="totalConfirmers() === 1">
|
||||||
@@ -136,41 +136,47 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-bold text-2xl mt-8">Claim</h2>
|
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2>
|
||||||
<pre>{{ util.inspect(veriClaim, false, null) }}</pre>
|
<pre
|
||||||
|
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
|
||||||
|
>{{ veriClaimDump }}</pre
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="font-bold text-2xl mt-8">Full Claim</h2>
|
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2>
|
||||||
<p>
|
<p class="mb-4">
|
||||||
The full claim includes the claim as it was originally issued, including
|
The full claim includes the claim as it was originally issued, including
|
||||||
the signature (ie. the proof of issuance by that person).
|
the signature (ie. the proof of issuance by that person).
|
||||||
</p>
|
</p>
|
||||||
<div v-if="!fullClaim">
|
<div v-if="!fullClaim">
|
||||||
<div v-if="fullClaimMessage">
|
<p v-if="fullClaimMessage" class="mb-4">
|
||||||
{{ fullClaimMessage }}
|
{{ fullClaimMessage }}
|
||||||
</div>
|
</p>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
|
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
@click="showFullClaim(veriClaim.id)"
|
@click="showFullClaim(veriClaim.id)"
|
||||||
>
|
>
|
||||||
Load Full Claim Details
|
Load Full Claim Details
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<pre>{{ util.inspect(fullClaim, false, null) }}</pre>
|
<pre>{{ fullClaimDump }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a :href="apiServer + '/api/claim/' + veriClaim.id" target="_blank">
|
<a
|
||||||
<button class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4">
|
:href="apiServer + '/api/claim/' + veriClaim.id"
|
||||||
View on the Public Server
|
target="_blank"
|
||||||
</button>
|
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
View on the Public Server
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||||
|
import * as yaml from "js-yaml";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
@@ -208,11 +214,14 @@ export default class ClaimView extends Vue {
|
|||||||
confsVisibleErrorMessage = "";
|
confsVisibleErrorMessage = "";
|
||||||
confsVisibleToIdList = []; // list of DIDs that can see any confirmer
|
confsVisibleToIdList = []; // list of DIDs that can see any confirmer
|
||||||
fullClaim = null;
|
fullClaim = null;
|
||||||
|
fullClaimDump = "";
|
||||||
fullClaimMessage = "";
|
fullClaimMessage = "";
|
||||||
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
|
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
|
||||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
|
veriClaimDump = "";
|
||||||
|
|
||||||
util = util;
|
util = util;
|
||||||
|
yaml = yaml;
|
||||||
containsHiddenDid = serverUtil.containsHiddenDid;
|
containsHiddenDid = serverUtil.containsHiddenDid;
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
@@ -301,6 +310,7 @@ export default class ClaimView extends Vue {
|
|||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.veriClaim = resp.data;
|
this.veriClaim = resp.data;
|
||||||
|
this.veriClaimDump = yaml.dump(this.veriClaim);
|
||||||
} else {
|
} else {
|
||||||
// actually, axios typically throws an error so we never get here
|
// actually, axios typically throws an error so we never get here
|
||||||
console.log("Error getting claim:", resp);
|
console.log("Error getting claim:", resp);
|
||||||
@@ -380,6 +390,7 @@ export default class ClaimView extends Vue {
|
|||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.fullClaim = resp.data;
|
this.fullClaim = resp.data;
|
||||||
|
this.fullClaimDump = yaml.dump(this.fullClaim);
|
||||||
} else {
|
} else {
|
||||||
// actually, axios typically throws an error so we never get here
|
// actually, axios typically throws an error so we never get here
|
||||||
console.log("Error getting full claim:", resp);
|
console.log("Error getting full claim:", resp);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<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 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1
|
<h1
|
||||||
@@ -25,6 +25,13 @@
|
|||||||
<span class="justify-around">(Only 50 most recent)</span>
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
<span />
|
<span />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-around">
|
||||||
|
<span />
|
||||||
|
<span class="justify-around">
|
||||||
|
(This does not include claims by them if they're not visible to you.)
|
||||||
|
</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<table
|
<table
|
||||||
@@ -124,7 +131,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
export default class ContactsView extends Vue {
|
export default class ContactAmountssView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
@@ -178,6 +185,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log("Error retrieving settings or gives.", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -185,7 +193,7 @@ export default class ContactsView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.userMessage ||
|
err.userMessage ||
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
"There was an error retrieving your settings and/or contacts and/or gives.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Home"></QuickNav>
|
<QuickNav selected="Home"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -145,6 +145,7 @@ export default class ContactGiftingView extends Vue {
|
|||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log("Error retrieving settings & contacts:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -152,7 +153,7 @@ export default class ContactGiftingView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.message ||
|
err.message ||
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
"There was an error retrieving your settings and/or contacts.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
@@ -44,30 +44,39 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-center" v-else>
|
<div class="text-center" v-else>
|
||||||
You have no identitifiers yet, so
|
You have no identitifiers yet, so
|
||||||
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
<router-link
|
||||||
|
:to="{ name: 'start' }"
|
||||||
|
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
||||||
|
>
|
||||||
create your identifier.
|
create your identifier.
|
||||||
</router-link>
|
</router-link>
|
||||||
<br />
|
<br />
|
||||||
We recommend you do that first; otherwise, these contacts won't see your
|
If you don't that first, these contacts won't see your activity.
|
||||||
activity.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
|
<div class="text-center">
|
||||||
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
|
||||||
|
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
||||||
|
<span>
|
||||||
|
If you do not see a scanning camera window here, check your camera
|
||||||
|
permissions.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as didJwt from "did-jwt";
|
||||||
|
import { sha256 } from "ethereum-cryptography/sha256.js";
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { QrcodeStream } from "vue-qrcode-reader";
|
import { QrcodeStream } from "vue-qrcode-reader";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import * as R from "ramda";
|
import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto";
|
||||||
import { SimpleSigner } from "@/libs/crypto";
|
|
||||||
import * as didJwt from "did-jwt";
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
import {
|
import {
|
||||||
@@ -131,6 +140,14 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const publicKeyHex = identity.keys[0].publicKeyHex;
|
const publicKeyHex = identity.keys[0].publicKeyHex;
|
||||||
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
||||||
|
|
||||||
|
const newDerivPath = nextDerivationPath(account.derivationPath);
|
||||||
|
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
|
||||||
|
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
||||||
|
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
||||||
|
const nextPublicEncKeyHashBase64 =
|
||||||
|
Buffer.from(nextPublicEncKeyHash).toString("base64");
|
||||||
|
|
||||||
const contactInfo = {
|
const contactInfo = {
|
||||||
iat: Date.now(),
|
iat: Date.now(),
|
||||||
iss: this.activeDid,
|
iss: this.activeDid,
|
||||||
@@ -139,6 +156,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
(settings?.firstName || "") +
|
(settings?.firstName || "") +
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
||||||
publicEncKey,
|
publicEncKey,
|
||||||
|
nextPublicEncKeyHash: nextPublicEncKeyHashBase64,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<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 max-w-3xl mx-auto">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Your Contacts
|
Your Contacts
|
||||||
@@ -9,17 +9,17 @@
|
|||||||
<div class="flex justify-between py-2">
|
<div class="flex justify-between py-2">
|
||||||
<span />
|
<span />
|
||||||
<span>
|
<span>
|
||||||
<router-link
|
<a
|
||||||
:to="{ name: 'help' }"
|
@click="showHintsForOnboarding()"
|
||||||
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
|
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
|
||||||
>
|
>
|
||||||
Help
|
Onboarding Hints
|
||||||
</router-link>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Contact -->
|
<!-- New Contact -->
|
||||||
<div class="mb-4 flex items-stretch">
|
<div class="mt-4 mb-4 flex items-stretch">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contact-qr' }"
|
:to="{ name: 'contact-qr' }"
|
||||||
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
|
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="DID, Name, Public Key (base 16 or 64)"
|
placeholder="DID, Name, Public Key, Next Public Key Hash"
|
||||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
||||||
v-model="contactInput"
|
v-model="contactInput"
|
||||||
/>
|
/>
|
||||||
@@ -109,12 +109,16 @@
|
|||||||
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
||||||
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-sm truncate" v-if="contact.nextPubKeyHashB64">
|
||||||
|
Next Public Key Hash (base 64):
|
||||||
|
{{ contact.nextPubKeyHashB64 }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
||||||
<div v-if="activeDid">
|
<div v-if="activeDid">
|
||||||
<button
|
<button
|
||||||
v-if="contact.seesMe"
|
v-if="contact.seesMe"
|
||||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
@click="setVisibility(contact, false, true)"
|
@click="setVisibility(contact, false, true)"
|
||||||
title="They can see you"
|
title="They can see you"
|
||||||
>
|
>
|
||||||
@@ -122,14 +126,14 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
@click="setVisibility(contact, true, true)"
|
@click="setVisibility(contact, true, true)"
|
||||||
title="They cannot see you"
|
title="They cannot see you"
|
||||||
>
|
>
|
||||||
<fa icon="eye-slash" class="fa-fw" />
|
<fa icon="eye-slash" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
@click="checkVisibility(contact)"
|
@click="checkVisibility(contact)"
|
||||||
title="Check Visibility"
|
title="Check Visibility"
|
||||||
v-if="activeDid"
|
v-if="activeDid"
|
||||||
@@ -140,19 +144,14 @@
|
|||||||
@click="register(contact)"
|
@click="register(contact)"
|
||||||
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
|
||||||
v-if="activeDid"
|
v-if="activeDid"
|
||||||
|
title="Registration"
|
||||||
>
|
>
|
||||||
<fa
|
<fa
|
||||||
v-if="contact.registered"
|
v-if="contact.registered"
|
||||||
icon="person-circle-check"
|
icon="person-circle-check"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
title="Registered"
|
|
||||||
/>
|
|
||||||
<fa
|
|
||||||
v-else
|
|
||||||
icon="person-circle-question"
|
|
||||||
class="fa-fw"
|
|
||||||
title="Registration Unknown"
|
|
||||||
/>
|
/>
|
||||||
|
<fa v-else icon="person-circle-question" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -171,7 +170,7 @@
|
|||||||
<button
|
<button
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-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] || ''"
|
||||||
>
|
>
|
||||||
To:
|
To:
|
||||||
{{
|
{{
|
||||||
@@ -190,7 +189,7 @@
|
|||||||
<button
|
<button
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
|
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] || ''"
|
||||||
>
|
>
|
||||||
From:
|
From:
|
||||||
{{
|
{{
|
||||||
@@ -276,6 +275,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { ONBOARD_MESSAGE } from "@/libs/util";
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -367,6 +367,10 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadGives() {
|
async loadGives() {
|
||||||
|
if (!this.activeDid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleResponse = (
|
const handleResponse = (
|
||||||
resp: { status: number; data: { data: GiveServerRecord[] } },
|
resp: { status: number; data: { data: GiveServerRecord[] } },
|
||||||
descriptions: Record<string, string>,
|
descriptions: Record<string, string>,
|
||||||
@@ -401,11 +405,11 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Retrieval Error",
|
||||||
text:
|
text:
|
||||||
"Got an error retrieving your " +
|
"Got an error retrieving your " +
|
||||||
(useRecipient ? "given" : "received") +
|
(useRecipient ? "given" : "received") +
|
||||||
" time from the server.",
|
" data from the server.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -456,18 +460,31 @@ export default class ContactsView extends Vue {
|
|||||||
this.givenToMeConfirmed = givenToMeConfirmed;
|
this.givenToMeConfirmed = givenToMeConfirmed;
|
||||||
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("Error loading gives", error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Load Error",
|
||||||
text: error as string,
|
text: "Got an error loading your gives.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showHintsForOnboarding() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Onboard Someone",
|
||||||
|
text: ONBOARD_MESSAGE,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async onClickNewContact(): Promise<void> {
|
async onClickNewContact(): Promise<void> {
|
||||||
if (!this.contactInput) {
|
if (!this.contactInput) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -488,7 +505,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let did = this.contactInput;
|
let did = this.contactInput;
|
||||||
let name, publicKeyBase64;
|
let name, publicKeyInput, nextPublicKeyHashInput;
|
||||||
const commaPos1 = this.contactInput.indexOf(",");
|
const commaPos1 = this.contactInput.indexOf(",");
|
||||||
if (commaPos1 > -1) {
|
if (commaPos1 > -1) {
|
||||||
did = this.contactInput.substring(0, commaPos1).trim();
|
did = this.contactInput.substring(0, commaPos1).trim();
|
||||||
@@ -496,15 +513,31 @@ export default class ContactsView extends Vue {
|
|||||||
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
|
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
|
||||||
if (commaPos2 > -1) {
|
if (commaPos2 > -1) {
|
||||||
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
||||||
publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim();
|
publicKeyInput = this.contactInput.substring(commaPos2 + 1).trim();
|
||||||
|
const commaPos3 = this.contactInput.indexOf(",", commaPos2 + 1);
|
||||||
|
if (commaPos3 > -1) {
|
||||||
|
publicKeyInput = this.contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
|
||||||
|
nextPublicKeyHashInput = this.contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// help with potential mistakes while this sharing requires copy-and-paste
|
// help with potential mistakes while this sharing requires copy-and-paste
|
||||||
|
let publicKeyBase64 = publicKeyInput;
|
||||||
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
||||||
// it must be all hex (compressed public key), so convert
|
// it must be all hex (compressed public key), so convert
|
||||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
||||||
}
|
}
|
||||||
const newContact = { did, name, publicKeyBase64 };
|
let nextPubKeyHashB64 = nextPublicKeyHashInput;
|
||||||
|
if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
|
||||||
|
// it must be all hex (compressed public key), so convert
|
||||||
|
nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier
|
||||||
|
}
|
||||||
|
const newContact = {
|
||||||
|
did,
|
||||||
|
name,
|
||||||
|
publicKeyBase64,
|
||||||
|
nextPubKeyHashB64: nextPubKeyHashB64,
|
||||||
|
};
|
||||||
await this.addContact(newContact);
|
await this.addContact(newContact);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,6 +558,7 @@ export default class ContactsView extends Vue {
|
|||||||
return this.addContact({
|
return this.addContact({
|
||||||
did: payload.iss,
|
did: payload.iss,
|
||||||
name: payload.own.name,
|
name: payload.own.name,
|
||||||
|
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
||||||
publicKeyBase64: payload.own.publicEncKey,
|
publicKeyBase64: payload.own.publicEncKey,
|
||||||
} as Contact);
|
} as Contact);
|
||||||
}
|
}
|
||||||
@@ -556,10 +590,20 @@ export default class ContactsView extends Vue {
|
|||||||
if (this.activeDid) {
|
if (this.activeDid) {
|
||||||
this.setVisibility(newContact, true, false);
|
this.setVisibility(newContact, true, false);
|
||||||
addedMessage =
|
addedMessage =
|
||||||
newContact.name +
|
"They were added, and your activity is visible to them.";
|
||||||
" was added, and your activity is visible to them.";
|
|
||||||
} else {
|
} else {
|
||||||
addedMessage = newContact.name + " was added.";
|
addedMessage = "They were added.";
|
||||||
|
}
|
||||||
|
if (this.isRegistered) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "New User?",
|
||||||
|
text: "If they are a new user, be sure to register them.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -568,32 +612,24 @@ export default class ContactsView extends Vue {
|
|||||||
title: "Contact Added",
|
title: "Contact Added",
|
||||||
text: addedMessage,
|
text: addedMessage,
|
||||||
},
|
},
|
||||||
5000,
|
-1, // keeping it up so that the "visibility" message is seen
|
||||||
);
|
);
|
||||||
if (this.isRegistered) {
|
|
||||||
// putting this last so that it shows on the top
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "info",
|
|
||||||
title: "New User?",
|
|
||||||
text:
|
|
||||||
"If " +
|
|
||||||
newContact.name +
|
|
||||||
" is a new user, be sure to register them.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Error when adding contact to storage:", err);
|
console.error("Error when adding contact to storage:", err);
|
||||||
|
let message = "An error prevented this import.";
|
||||||
|
if (
|
||||||
|
err.message?.indexOf("Key already exists in the object store.") > -1
|
||||||
|
) {
|
||||||
|
message =
|
||||||
|
"A contact with that DID is already in your contact list. Edit them directly below.";
|
||||||
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Contact Not Added",
|
title: "Contact Not Added",
|
||||||
text: "An error prevented this import.",
|
text: message,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -705,6 +741,7 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error when registering:", error);
|
||||||
let userMessage = "There was an error. See logs for more info.";
|
let userMessage = "There was an error. See logs for more info.";
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
@@ -721,7 +758,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Registration Error",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
@@ -736,62 +773,70 @@ export default class ContactsView extends Vue {
|
|||||||
visibility: boolean,
|
visibility: boolean,
|
||||||
showSuccessAlert: boolean,
|
showSuccessAlert: boolean,
|
||||||
) {
|
) {
|
||||||
const url =
|
const visibilityPrompt =
|
||||||
this.apiServer +
|
showSuccessAlert &&
|
||||||
"/api/report/" +
|
(visibility
|
||||||
(visibility ? "canSeeMe" : "cannotSeeMe");
|
? "Are you sure you want to make your activity visible to them?"
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
: "Are you sure you want to hide all your activity from them?");
|
||||||
const headers = await this.getHeaders(identity);
|
if (visibilityPrompt && confirm(visibilityPrompt)) {
|
||||||
const payload = JSON.stringify({ did: contact.did });
|
const url =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/report/" +
|
||||||
|
(visibility ? "canSeeMe" : "cannotSeeMe");
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
const headers = await this.getHeaders(identity);
|
||||||
|
const payload = JSON.stringify({ did: contact.did });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
if (showSuccessAlert) {
|
if (showSuccessAlert) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Visibility Set",
|
||||||
|
text:
|
||||||
|
this.nameForDid(this.contacts, contact.did) +
|
||||||
|
" can " +
|
||||||
|
(visibility ? "" : "not ") +
|
||||||
|
"see your activity.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
contact.seesMe = visibility;
|
||||||
|
db.contacts.update(contact.did, { seesMe: visibility });
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Got some bad server response when setting visibility: ",
|
||||||
|
resp.status,
|
||||||
|
resp,
|
||||||
|
);
|
||||||
|
const message =
|
||||||
|
resp.data.error?.message || "Got some error setting visibility.";
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "success",
|
type: "danger",
|
||||||
title: "Visibility Set",
|
title: "Error Setting Visibility",
|
||||||
text:
|
text: message,
|
||||||
this.nameForDid(this.contacts, contact.did) +
|
|
||||||
" can " +
|
|
||||||
(visibility ? "" : "not ") +
|
|
||||||
"see your activity.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
contact.seesMe = visibility;
|
} catch (err) {
|
||||||
db.contacts.update(contact.did, { seesMe: visibility });
|
console.error("Got some error when setting visibility:", err);
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Got some bad server response when setting visibility: ",
|
|
||||||
resp,
|
|
||||||
);
|
|
||||||
const message =
|
|
||||||
resp.data.error?.message || "Bad server response of " + resp.status;
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error Setting Visibility",
|
||||||
text: message,
|
text: "Check connectivity and try again.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error("Got some server error when setting visibility:", err);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Server Error",
|
|
||||||
text: "Check connectivity and try again.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,19 +875,19 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error Checking Visibility",
|
||||||
text: message,
|
text: message,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Caught error from server request to check visibility:", err);
|
console.log("Caught error from request to check visibility:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error Checking Visibility",
|
||||||
text: "Check connectivity and try again.",
|
text: "Check connectivity and try again.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
@@ -901,13 +946,13 @@ export default class ContactsView extends Vue {
|
|||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
} else if (!parseFloat(this.hourInput)) {
|
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Input Error",
|
title: "Input Error",
|
||||||
text: "Giving 0 hours does nothing.",
|
text: "Giving no hours or descrption does nothing.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -1028,6 +1073,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("Error in createAndSubmitGive: ", error);
|
||||||
let userMessage = "There was an error. See logs for more info.";
|
let userMessage = "There was an error. See logs for more info.";
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
@@ -1044,7 +1090,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Server Error",
|
title: "Error Sending Give",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Discover"></QuickNav>
|
<QuickNav selected="Discover"></QuickNav>
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Discover
|
Discover
|
||||||
@@ -136,6 +137,7 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
|
|||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -149,6 +151,7 @@ interface Notification {
|
|||||||
QuickNav,
|
QuickNav,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
|
TopMessage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {
|
export default class DiscoverView extends Vue {
|
||||||
@@ -274,8 +277,8 @@ export default class DiscoverView extends Vue {
|
|||||||
const plans: ProjectData[] = results.data;
|
const plans: ProjectData[] = results.data;
|
||||||
if (plans) {
|
if (plans) {
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, rowid });
|
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||||
}
|
}
|
||||||
this.remoteCount = this.projects.length;
|
this.remoteCount = this.projects.length;
|
||||||
} else {
|
} else {
|
||||||
@@ -357,8 +360,14 @@ export default class DiscoverView extends Vue {
|
|||||||
if (beforeId) {
|
if (beforeId) {
|
||||||
const plans: ProjectData[] = results.data;
|
const plans: ProjectData[] = results.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId = plan.handleId, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, rowid });
|
this.projects.push({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
handleId,
|
||||||
|
issuerDid,
|
||||||
|
rowid,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.projects = results.data;
|
this.projects = results.data;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<QuickNav />
|
<QuickNav />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
@@ -21,35 +21,280 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- eslint-disable prettier/prettier -->
|
||||||
<div>
|
<div>
|
||||||
<p>Here are things to try to get notifications working.</p>
|
<p>Here are ways to test notifications and get them working.</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Test</h2>
|
<h2 class="text-xl font-semibold mt-4">Full Test</h2>
|
||||||
<p>Somehow call the service-worker self.showNotification</p>
|
<div>
|
||||||
|
<p>
|
||||||
|
If this works then you're all set.
|
||||||
|
<button
|
||||||
|
@click="sendTestWebPushMessage(true)"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Send Yourself a Test Web Push Message (Through Push Server but
|
||||||
|
Skipping Client Filter)
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Check OS-level permissions</h2>
|
<h2 class="text-xl font-semibold mt-4">
|
||||||
|
If this app doesn't support notifications...
|
||||||
|
<!-- Note that that exact verbiage shows in a message elsewhere. -->
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
To be notified of interesting updates, install this app on your device
|
||||||
|
(as opposed to using it inside the browser app). In Chrome, it may prompt
|
||||||
|
you, and you can also look for the "Install" command in the browser
|
||||||
|
settings; on the the desktop, look for this icon in the address bar:
|
||||||
|
<img
|
||||||
|
src="../assets/help/chrome-install-pwa.png"
|
||||||
|
alt="Chrome 'install' icon"
|
||||||
|
class="ml-4"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold mt-4">
|
||||||
|
If you must enable notifications...
|
||||||
|
<!-- Note that that exact verbiage shows in a message elsewhere. -->
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<button class="text-blue-500" @click="showNotificationChoice()">
|
||||||
|
Click here.
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold mt-4">
|
||||||
|
If you're waiting for system initialization...
|
||||||
|
<!-- Note that that exact verbiage shows in a message elsewhere. -->
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
... and it never stops, then there is a problem with the underlying
|
||||||
|
service worker or push server mechanism in your browser. Your best bet
|
||||||
|
is to follow the "Reinstall" steps below or use a different browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold mt-4">Check App Permissions</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
In Apple iOS, check "Settings" -> "Notifications", look for the Time
|
||||||
|
Safari app (or the browser you're using), and make sure notifications
|
||||||
|
are enabled.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In Android, hold on to the app icon, then select "App Info", then
|
||||||
|
"Notifications" and make sure they're enabled. If it's still a problem
|
||||||
|
then go further:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you installed the app with Chrome, make sure there are no other
|
||||||
|
tabs with it open. Here are some ways to clear caches that can mess
|
||||||
|
things up (and note that this clears out data from the installed app
|
||||||
|
-- which is good to do while the app is installed):
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="list-disc ml-4">
|
||||||
|
Go to Chrome "App Info", then "Storage & Cache" and "Clear Storage".
|
||||||
|
</li>
|
||||||
|
<li class="list-disc ml-4">
|
||||||
|
Go to Chrome "Settings", then "Privacy and Security" and "Clear
|
||||||
|
"Clear browsing data", then "Cookies and site data". Make sure the
|
||||||
|
"Time Range" at the top shows "All time".
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
On a Mac, go to "Settings" and check "Notifications".
|
||||||
|
<img
|
||||||
|
src="../assets/help/mac-installed-app-settings.png"
|
||||||
|
alt="Mac app settings"
|
||||||
|
class="ml-4"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold mt-4">Check Browser Permissions</h2>
|
||||||
|
<div>
|
||||||
|
<p>In Apple iOS, check Settings -> Notifications.</p>
|
||||||
|
<p>In Android, check Settings -> Notifications.</p>
|
||||||
|
|
||||||
|
You can find more details about compatibility
|
||||||
|
<a
|
||||||
|
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
|
||||||
|
class="text-blue-500"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold mt-4">
|
||||||
|
Check Operating System (OS) Permissions
|
||||||
|
</h2>
|
||||||
|
<div class="px-2">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
|
||||||
|
<div>
|
||||||
|
Notifications require iOS 16.4 or higher. To check your iOS version,
|
||||||
|
go to Settings > General > About > Software Version.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3>
|
||||||
|
<div>
|
||||||
|
We recommend Chrome. It must be version 42 or higher. Check your
|
||||||
|
version under Settings -> About Chrome.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold">Desktop - Mac</h3>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
See "System Settings" -> "Notifications" and make sure it is
|
||||||
|
enabled for the browser you're using. Note that these
|
||||||
|
notifications require Mac OS 13; see your macOS version under
|
||||||
|
Apple -> "About This Mac".
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold">Desktop - Windows</h3>
|
||||||
|
In Windows, check "Settings" -> "Notifications".
|
||||||
|
<img
|
||||||
|
src="../assets/help/windows-system-enable-notifications.png"
|
||||||
|
alt="Windows system settings"
|
||||||
|
class="ml-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
You can find more details about compatibility
|
||||||
|
<a
|
||||||
|
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
|
||||||
|
class="text-blue-500"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold mt-4">Reinstall</h2>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
If all else fails, uninstall the app, ensure all the browser tabs with
|
||||||
|
it are closed, and clear out caches and storage.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Of course, you'll want to back up all your data first -- all seeds as
|
||||||
|
well as the contacts & settings -- on the Account
|
||||||
|
<fa icon="circle-user" /> page.
|
||||||
|
</p>
|
||||||
|
<ul class="ml-4 list-disc">
|
||||||
|
<li>
|
||||||
|
Clear cache.
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
In mobile, look for the browser app settings. This is true even
|
||||||
|
for an installed app: go to the browser which you used to
|
||||||
|
initially visit timesafari.app, because those settings affect
|
||||||
|
the app. Look for "Delete browsing data" in the "Settings",
|
||||||
|
under "Privacy and Security".
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
In Chrome, go to `chrome://settings/cookies` and "all site data
|
||||||
|
and permissions" for timesafari.app; in Firefox, go to
|
||||||
|
`about:preferences` and search for "cache" then "Manage Data"
|
||||||
|
for timesafari.app. Also manually remove the IndexedDB data if
|
||||||
|
the DBs still show.)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Clear notification permission. (In Chrome, go to
|
||||||
|
`chrome://settings/content/notifications`; in Firefox, go to
|
||||||
|
`about:preferences` and search for "notifications".)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Unregister service worker. (In Chrome, go to
|
||||||
|
`chrome://serviceworker-internals/`; in Firefox, go to
|
||||||
|
`about:serviceworkers`.)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Clear "Cache Storage". (In Chrome, in dev tools under "Application";
|
||||||
|
in Firefox, in dev tools under "Storage".)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>Then reinstall the app.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold mt-4">Tests</h2>
|
||||||
|
<button
|
||||||
|
@click="showTestNotification()"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
||||||
|
>
|
||||||
|
Send Test Notification Directly to Device (Not Through Push Server)
|
||||||
|
</button>
|
||||||
<p>
|
<p>
|
||||||
Walk-throughs & screenshots, maybe for all combinations of OS &
|
If that didn't show a notification on your device, the problem is that
|
||||||
browsers.
|
your browser or your operating system are not allowing notifications
|
||||||
|
through. See "Check App Permissions" and "Check Browser Permissions" and
|
||||||
|
"Check Operating System (OS) Permissions" above.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Check browser-level permissions</h2>
|
<button
|
||||||
<p>Walk-throughs & screenshots for browser settings</p>
|
@click="alertWebPushSubscription()"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
||||||
<h2 class="text-xl font-semibold">Explain full reset to start again</h2>
|
>
|
||||||
|
Show Web Push Subscription Info
|
||||||
|
</button>
|
||||||
<p>
|
<p>
|
||||||
Walk-throughs for clearing everything & subscribing anew to get a
|
If that showed "null" then the notification is not active.
|
||||||
message
|
<button class="text-blue-500" @click="showNotificationChoice()">
|
||||||
|
Click here.
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Auto-detection</h2>
|
<button
|
||||||
<p>Show results of auto-detection whether they're turned on</p>
|
@click="sendTestWebPushMessage(true)"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
||||||
|
>
|
||||||
|
Send Yourself a Test Web Push Message (Through Push Server but Skipping
|
||||||
|
Client Filter)
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
If that didn't show a notification on your device, there is a problem
|
||||||
|
getting to the push server. Disable notifications and then enable them
|
||||||
|
again.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="sendTestWebPushMessage()"
|
||||||
|
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
||||||
|
>
|
||||||
|
Send Yourself a Test Web Push Message (Through Push Server and Client
|
||||||
|
Filter)
|
||||||
|
</button>
|
||||||
|
<p>
|
||||||
|
If you don't see a message, it could be that there is nothing new for
|
||||||
|
you to see. If the previous test worked, then things should work fine.
|
||||||
|
If you notice a full 24 hours where you get no notification and you know
|
||||||
|
that there are new items that should show, gather as many details as
|
||||||
|
possible and go to the bottom of
|
||||||
|
<router-link to="help" class="text-blue-500"> this page </router-link>
|
||||||
|
for ways to contact us.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- eslint-enable -->
|
||||||
</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 QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import { sendTestThroughPushServer } from "@/libs/util";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -61,5 +306,112 @@ interface Notification {
|
|||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
export default class HelpNotificationsView extends Vue {
|
export default class HelpNotificationsView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
subscription: PushSubscription | null = null;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
this.subscription = await registration.pushManager.getSubscription();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Mount error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alertWebPushSubscription() {
|
||||||
|
console.log(
|
||||||
|
"Web push subscription:",
|
||||||
|
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
|
||||||
|
);
|
||||||
|
alert(JSON.stringify(this.subscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTestWebPushMessage(skipFilter: boolean = false) {
|
||||||
|
if (!this.subscription) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Not Subscribed",
|
||||||
|
// Note that this exact verbiage shows in help text.
|
||||||
|
text: "You must enable notifications before testing the web push.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendTestThroughPushServer(this.subscription, skipFilter);
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Test Web Push Sent",
|
||||||
|
text:
|
||||||
|
"Check your device for the test web push message" +
|
||||||
|
(skipFilter ? "." : " if there are new items in your feed."),
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Got an error sending test notification:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Sending Test",
|
||||||
|
text: "Got an error sending the test web push notification.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showTestNotification() {
|
||||||
|
const TEST_NOTIFICATION_TITLE = "It Worked";
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then((registration) => {
|
||||||
|
return registration.showNotification(TEST_NOTIFICATION_TITLE, {
|
||||||
|
body: "This is your test notification.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Sent",
|
||||||
|
text: `A notification was triggered, so one should show on your device entitled '${TEST_NOTIFICATION_TITLE}'.`,
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Got a notification error:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Failed",
|
||||||
|
text: "Got an error sending a notification.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotificationChoice() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "notification-permission",
|
||||||
|
title: "", // unused, only here to satisfy type check
|
||||||
|
text: "", // unused, only here to satisfy type check
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<QuickNav />
|
<QuickNav />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
@@ -21,25 +21,27 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- eslint-disable prettier/prettier -->
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
This app is a window into data that you and your friends own, focused on
|
This app is a window into data that you and your friends own, focused on
|
||||||
gifts and collaboration.
|
gifts and collaboration.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
|
<h2 class="text-xl font-semibold">What is the idea here?</h2>
|
||||||
<p>
|
<p>
|
||||||
We are building networks of people who want to grow a giving society.
|
We are building networks of people who want to grow a giving society.
|
||||||
First of all, you can record ways you've seen people give, and that
|
First of all, you can see what people have given, and also recognize
|
||||||
leaves a permanent record -- one that came from you, and the recipient
|
gifts you've seen, in a way that leaves a permanent record -- one that
|
||||||
can prove it was for them. This is personally gratifying, but it extends
|
came from you, and the recipient can prove it was for them. This is
|
||||||
to broader work: volunteers can get confirmation of activity and
|
personally gratifying, but it extends to broader work: volunteers get
|
||||||
selectively show off their contributions and network.
|
confirmation of activity, and selectively show off their contributions
|
||||||
|
and network.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can also record projects and plans and invite others to collaborate.
|
You can show giving and also offer help to ideas, based on others'
|
||||||
Soon you'll be able to see when others are interested and see how much
|
willingness to help out, too. You can record your own ideas and invite
|
||||||
they're willing to contribute, even if there are conditions.
|
others to collaborate.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This app uses the power of cryptography to build a reputation, recording
|
This app uses the power of cryptography to build a reputation, recording
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I backup my identifier (secret) data?
|
How do I backup my identifier (secret) data?
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
||||||
</li>
|
</li>
|
||||||
@@ -109,7 +111,7 @@
|
|||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I backup my other (non-identifier-secret) data?
|
How do I backup my other (non-identifier-secret) data?
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
||||||
</li>
|
</li>
|
||||||
@@ -131,7 +133,7 @@
|
|||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I restore my identifier (secret) data?
|
How do I restore my identifier (secret) data?
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
<router-link class="text-blue-500" to="/import-account">
|
<router-link class="text-blue-500" to="/import-account">
|
||||||
Go to the import page
|
Go to the import page
|
||||||
@@ -143,12 +145,10 @@
|
|||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I restore my other (non-identifier-secret) data?
|
How do I restore my other (non-identifier-secret) data?
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
Make sure you have your backup file (above), then contact us with
|
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
|
||||||
your interest. This is functionality that has to be written, and
|
click Advanced, and follow the instructions for Data Import.
|
||||||
your interest will help us prioritize it, but there are also manual
|
|
||||||
ways to restore your data.
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,6 +163,47 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">How do I erase my data?</h2>
|
||||||
|
<p>
|
||||||
|
Before doing this, note the two kinds of data to backup: identity data,
|
||||||
|
and other data for contacts and settings (see instructions above).
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li class="list-disc list-outside ml-4">
|
||||||
|
Mobile
|
||||||
|
<ul>
|
||||||
|
<li class="list-disc list-outside ml-4">
|
||||||
|
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
|
||||||
|
</li>
|
||||||
|
<li class="list-disc list-outside ml-4">
|
||||||
|
DuckDuckGo: long hold -> Clear Data (takes effect immediately)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="list-disc list-outside ml-4">
|
||||||
|
Desktop
|
||||||
|
<ul>
|
||||||
|
<li class="list-disc list-outside ml-4">
|
||||||
|
Chrome:
|
||||||
|
<a href="chrome://settings/content/all" class="text-blue-500"
|
||||||
|
>clear here</a
|
||||||
|
>
|
||||||
|
also clear under dev tools Application
|
||||||
|
</li>
|
||||||
|
<li class="list-disc list-outside ml-4">
|
||||||
|
Firefox: <a href="about:preferences">go here</a>, Manage Data,
|
||||||
|
find timesafari.app and select, hit Remove Selected, then Save
|
||||||
|
Changes
|
||||||
|
</li>
|
||||||
|
<li class="list-disc list-outside ml-4">
|
||||||
|
Safari: Settings -> Privacy -> Manage Website Data, search for
|
||||||
|
timesafari.app and select, hit Remove Selected, then Done.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>To erase your data from our servers, contact us (below).</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>
|
||||||
@@ -181,6 +222,15 @@
|
|||||||
different page.
|
different page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
Where do I get help with notifications?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<router-link class="text-blue-500" to="/help-notifications"
|
||||||
|
>Here.</router-link
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I access even more functionality?
|
How do I access even more functionality?
|
||||||
</h2>
|
</h2>
|
||||||
@@ -196,11 +246,31 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
|
||||||
<p>
|
<p style="display:inline; align-items: center">
|
||||||
See
|
This work is marked with
|
||||||
|
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
|
||||||
|
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
||||||
|
<img
|
||||||
|
src="../assets/help/creative-commons-circle.svg"
|
||||||
|
alt="CC circle"
|
||||||
|
width="20"
|
||||||
|
class="display: inline"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="../assets/help/creative-commons-zero.svg"
|
||||||
|
alt="CC zero"
|
||||||
|
width="20"
|
||||||
|
style="display: inline"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
For notifications, this service stores push token data; that can be revoked at any time
|
||||||
|
by disabling notifications on the Account <fa icon="circle-user" class="fa-fw" /> page.
|
||||||
|
<br />
|
||||||
|
For all other claim data,
|
||||||
<a href="https://endorser.ch/privacy-policy" class="text-blue-500">
|
<a href="https://endorser.ch/privacy-policy" class="text-blue-500">
|
||||||
the Endorser Service Privacy Policy.
|
the Endorser Service has this Privacy Policy.
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -221,16 +291,21 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Contact us at
|
Contact us at
|
||||||
<a mailto="info@TimeSafari.app">info@TimeSafari.app</a>
|
<a href="mailto:info@TimeSafari.app" class="text-blue-500"
|
||||||
|
>info@TimeSafari.app</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- eslint enable -->
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Package from "../../package.json";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
import * as Package from "../../package.json";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import { ONBOARD_MESSAGE } from "@/libs/util";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -252,8 +327,7 @@ export default class Help extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Onboard Someone",
|
title: "Onboard Someone",
|
||||||
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, preferably).
|
text: ONBOARD_MESSAGE,
|
||||||
text: "1) Check that they've entered their name. 2) Go to the scanning page via the Identity page and then the through the QR icon at the top, and then scan and register them. 3) Have them go to that page and scan you.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Home"></QuickNav>
|
<QuickNav selected="Home"></QuickNav>
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<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">
|
||||||
Time Safari
|
Time Safari
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<!-- prompt to install notifications -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div
|
||||||
|
v-if="!notificationsSupported()"
|
||||||
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
|
>
|
||||||
|
<p style="display: inline; align-items: center">
|
||||||
|
This app doesn't support notifications, so let's fix that. <br />
|
||||||
|
<!-- Note that that exact verbiage shows in the help. -->
|
||||||
|
|
||||||
|
<span v-if="userAgentInfo.getOS().name === 'iOS'">
|
||||||
|
Tap on "Share"<img
|
||||||
|
src="../assets/help/apple-share-icon.svg"
|
||||||
|
alt="Apple 'share' icon"
|
||||||
|
width="30"
|
||||||
|
style="display: inline; margin: 0 5px; vertical-align: middle"
|
||||||
|
/>and then "Add to Home Screen"
|
||||||
|
<fa icon="square-plus" title="Apple 'Add' icon" />
|
||||||
|
and go click on that new app.
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="userAgentInfo.getBrowser().name.startsWith('Chrome')"
|
||||||
|
>
|
||||||
|
You should see a prompt to install, or you can click on the
|
||||||
|
top-right dots
|
||||||
|
<fa
|
||||||
|
icon="ellipsis-vertical"
|
||||||
|
title="vertical ellipsis"
|
||||||
|
class="fa-fw"
|
||||||
|
/>
|
||||||
|
and then "Install"<img
|
||||||
|
src="../assets/help/install-android-chrome.png"
|
||||||
|
alt="Android 'install' icon"
|
||||||
|
width="30"
|
||||||
|
style="display: inline; margin: 0 5px; vertical-align: middle"
|
||||||
|
/>
|
||||||
|
and go use that app. If you already did these steps, reload this app
|
||||||
|
so that it is fully detected.
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
Try
|
||||||
|
<a href="https://www.google.com/chrome/" class="text-blue-500"
|
||||||
|
>Google Chrome</a
|
||||||
|
>
|
||||||
|
or look for a way to install as an app.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- show the actions for recognizing a give -->
|
<!-- show the actions for recognizing a give -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div
|
<div
|
||||||
@@ -13,11 +65,11 @@
|
|||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
<p class="text-lg mb-3">
|
<p class="text-lg mb-3">
|
||||||
You need an <b>identifier</b> before you can record others' giving.
|
You need an <b>identifier</b> before you can record anyone's gives.
|
||||||
</p>
|
</p>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'start' }"
|
:to="{ name: 'start' }"
|
||||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
Create Your Identifier</router-link
|
Create Your Identifier</router-link
|
||||||
>
|
>
|
||||||
@@ -27,17 +79,17 @@
|
|||||||
v-else-if="!isRegistered"
|
v-else-if="!isRegistered"
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
Someone must register your account before you can record others' giving.
|
Someone must register your account before you can record anyone's gives.
|
||||||
To do this:
|
To do this:
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contact-qr' }"
|
:to="{ name: 'contact-qr' }"
|
||||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
1. Show Them Your Identity Info</router-link
|
1. Show Them Your Identity Info</router-link
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'account' }"
|
:to="{ name: 'account' }"
|
||||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
2. Check Your Limits</router-link
|
2. Check Your Limits</router-link
|
||||||
>
|
>
|
||||||
@@ -103,42 +155,54 @@
|
|||||||
showGivenToUser="true"
|
showGivenToUser="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Results List -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
||||||
|
<InfiniteScroll @reached-bottom="loadMoreGives">
|
||||||
|
<ul class="border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
class="border-b border-slate-300 py-2"
|
||||||
|
v-for="record in feedData"
|
||||||
|
:key="record.jwtId"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
|
||||||
|
v-if="record.jwtId == feedLastViewedClaimId"
|
||||||
|
>
|
||||||
|
You've seen all the following
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
|
||||||
|
<span class="">{{ this.giveDescription(record) }}</span>
|
||||||
|
<a @click="onClickLoadClaim(record.jwtId)">
|
||||||
|
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</InfiniteScroll>
|
||||||
<div :class="{ hidden: isHiddenSpinner }">
|
<div :class="{ hidden: isHiddenSpinner }">
|
||||||
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||||
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ul class="border-t border-slate-300">
|
|
||||||
<li
|
|
||||||
class="border-b border-slate-300 py-2"
|
|
||||||
v-for="record in feedData"
|
|
||||||
:key="record.jwtId"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
|
|
||||||
v-if="record.jwtId == feedLastViewedId"
|
|
||||||
>
|
|
||||||
You've seen all the following
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
|
|
||||||
<span class="">{{ this.giveDescription(record) }}</span>
|
|
||||||
<a @click="onClickLoadClaim(record.jwtId)">
|
|
||||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { UAParser } from "ua-parser-js";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
@@ -146,11 +210,7 @@ import {
|
|||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -160,7 +220,13 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
components: {
|
||||||
|
GiftedDialog,
|
||||||
|
QuickNav,
|
||||||
|
EntityIcon,
|
||||||
|
InfiniteScroll,
|
||||||
|
TopMessage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -169,13 +235,13 @@ export default class HomeView extends Vue {
|
|||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
feedAllLoaded = false;
|
|
||||||
feedData = [];
|
feedData = [];
|
||||||
feedPreviousOldestId?: string;
|
feedPreviousOldestId?: string;
|
||||||
feedLastViewedId?: string;
|
feedLastViewedClaimId?: string;
|
||||||
isHiddenSpinner = true;
|
isHiddenSpinner = true;
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
@@ -212,11 +278,15 @@ export default class HomeView extends Vue {
|
|||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
|
|
||||||
|
// this returns a Promise but we don't need to wait for it
|
||||||
this.updateAllFeed();
|
this.updateAllFeed();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log("Error retrieving settings and/or feed.", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -224,13 +294,17 @@ export default class HomeView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.userMessage ||
|
err.userMessage ||
|
||||||
"There was an error retrieving the latest sweet, sweet action.",
|
"There was an error retrieving your settings and/or the latest activity.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notificationsSupported() {
|
||||||
|
return "Notification" in window;
|
||||||
|
}
|
||||||
|
|
||||||
public async buildHeaders() {
|
public async buildHeaders() {
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -257,24 +331,33 @@ export default class HomeView extends Vue {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data loader used by infinite scroller
|
||||||
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
|
**/
|
||||||
|
public async loadMoreGives(payload: boolean) {
|
||||||
|
if (payload) {
|
||||||
|
this.updateAllFeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async updateAllFeed() {
|
public async updateAllFeed() {
|
||||||
this.isHiddenSpinner = false;
|
this.isHiddenSpinner = false;
|
||||||
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
|
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
||||||
.then(async (results) => {
|
.then(async (results) => {
|
||||||
if (results.data.length > 0) {
|
if (results.data.length > 0) {
|
||||||
this.feedData = this.feedData.concat(results.data);
|
this.feedData = this.feedData.concat(results.data);
|
||||||
this.feedAllLoaded = results.hitLimit;
|
|
||||||
this.feedPreviousOldestId =
|
this.feedPreviousOldestId =
|
||||||
results.data[results.data.length - 1].jwtId;
|
results.data[results.data.length - 1].jwtId;
|
||||||
|
// The following update is only done on the first load.
|
||||||
if (
|
if (
|
||||||
this.feedLastViewedId == null ||
|
this.feedLastViewedClaimId == null ||
|
||||||
this.feedLastViewedId < results.data[0].jwtId
|
this.feedLastViewedClaimId < results.data[0].jwtId
|
||||||
) {
|
) {
|
||||||
await db.open();
|
await db.open();
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
lastViewedClaimId: results.data[0].jwtId,
|
lastViewedClaimId: results.data[0].jwtId,
|
||||||
});
|
});
|
||||||
// but not for this page because we need to remember what it was before
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -284,17 +367,22 @@ export default class HomeView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Export Error",
|
title: "Feed Error",
|
||||||
text: e.userMessage || "There was an error retrieving feed data.",
|
text: e.userMessage || "There was an error retrieving feed data.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isHiddenSpinner = true;
|
this.isHiddenSpinner = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
|
/**
|
||||||
|
* Retrieve claims in reverse chronological order
|
||||||
|
*
|
||||||
|
* @param beforeId the earliest ID (of previous searches) to search earlier
|
||||||
|
* @return claims in reverse chronological order
|
||||||
|
*/
|
||||||
|
public async retrieveGives(endorserApiServer: string, beforeId?: string) {
|
||||||
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -21,9 +21,6 @@
|
|||||||
<div class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4">
|
<div class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4">
|
||||||
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
||||||
<span class="overflow-hidden">
|
<span class="overflow-hidden">
|
||||||
<h2 class="text-xl font-semibold mb-0">
|
|
||||||
{{ givenName }}
|
|
||||||
</h2>
|
|
||||||
<div class="text-sm text-slate-500 truncate">
|
<div class="text-sm text-slate-500 truncate">
|
||||||
<b>ID:</b> <code>{{ activeDid }}</code>
|
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +87,6 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
public activeDid = "";
|
public activeDid = "";
|
||||||
public apiServer = "";
|
public apiServer = "";
|
||||||
public apiServerInput = "";
|
public apiServerInput = "";
|
||||||
public givenName = "";
|
|
||||||
public otherIdentities: Array<{ did: string }> = [];
|
public otherIdentities: Array<{ did: string }> = [];
|
||||||
public showContactGives = false;
|
public showContactGives = false;
|
||||||
|
|
||||||
@@ -111,9 +107,6 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.apiServerInput = settings?.apiServer || "";
|
this.apiServerInput = settings?.apiServer || "";
|
||||||
this.givenName =
|
|
||||||
(settings?.firstName || "") +
|
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
|
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -72,6 +72,7 @@ import {
|
|||||||
DEFAULT_ROOT_DERIVATION_PATH,
|
DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
deriveAddress,
|
deriveAddress,
|
||||||
newIdentifier,
|
newIdentifier,
|
||||||
|
nextDerivationPath,
|
||||||
} from "../libs/crypto";
|
} from "../libs/crypto";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
@@ -121,17 +122,7 @@ export default class ImportAccountView extends Vue {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// increment the last number in that max derivation path
|
// increment the last number in that max derivation path
|
||||||
let lastStr = accountWithMaxDeriv.derivationPath.split("/").slice(-1)[0];
|
const newDerivPath = nextDerivationPath(accountWithMaxDeriv.derivationPath);
|
||||||
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 mne: string = accountWithMaxDeriv.mnemonic;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Projects"></QuickNav>
|
<QuickNav selected="Projects"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
maxlength="5000"
|
maxlength="5000"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
||||||
{{ fullClaim.description.length }}/5000 max. characters
|
{{ fullClaim.description?.length }}/5000 max. characters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<QuickNav />
|
<QuickNav />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
@@ -88,7 +88,7 @@ export default class NewIdentifierView extends Vue {
|
|||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "home" });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav />
|
||||||
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>Map View
|
>Map View
|
||||||
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="url">
|
<div v-if="url">
|
||||||
@@ -88,8 +91,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div v-if="activeDid" class="mb-4">
|
||||||
<div v-if="activeDid" class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<button
|
||||||
@click="openOfferDialog({ name: 'you', did: activeDid })"
|
@click="openOfferDialog({ name: 'you', did: activeDid })"
|
||||||
class="block w-full 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"
|
||||||
@@ -99,17 +102,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div v-if="activeDid">
|
||||||
<div v-if="activeDid" class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<button
|
||||||
@click="openGiftDialog({ name: 'you', did: activeDid })"
|
@click="openGiftDialog({ name: 'you', did: activeDid })"
|
||||||
class="block w-full 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>
|
||||||
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
|
<p class="mt-2 mb-4 text-center">Or, record a contribution from:</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</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="openGiftDialog()">
|
<li @click="openGiftDialog()">
|
||||||
@@ -234,28 +236,32 @@
|
|||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
Contributions To This Idea
|
Contributions To This Idea
|
||||||
</h3>
|
</h3>
|
||||||
<ul>
|
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
||||||
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
|
<div class="text-center">
|
||||||
|
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
|
||||||
<button
|
<button
|
||||||
@click="onClickLoadProject(plan.handleId)"
|
@click="onClickLoadProject(plan.handleId)"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
{{ plan.name }}
|
{{ plan.name }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
Contributions By This Idea
|
Contributions By This Idea
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
||||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
<div class="text-center">
|
||||||
class="text-blue-500"
|
<button
|
||||||
>
|
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||||
{{ fulfilledByThis.name }}
|
class="text-blue-500"
|
||||||
</button>
|
>
|
||||||
|
{{ fulfilledByThis.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -279,6 +285,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import OfferDialog from "@/components/OfferDialog.vue";
|
import OfferDialog from "@/components/OfferDialog.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
@@ -303,7 +310,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
|
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
|
||||||
})
|
})
|
||||||
export default class ProjectViewView extends Vue {
|
export default class ProjectViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -356,12 +363,6 @@ export default class ProjectViewView extends Vue {
|
|||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first()) as Account;
|
.first()) as Account;
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error(
|
|
||||||
"Attempted to load project records with no identity available.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Projects"></QuickNav>
|
<QuickNav selected="Projects"></QuickNav>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<TopMessage />
|
||||||
|
|
||||||
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Your Ideas
|
Your Ideas
|
||||||
@@ -79,6 +81,7 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { ProjectData } from "@/libs/endorserServer";
|
import { ProjectData } from "@/libs/endorserServer";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
@@ -89,7 +92,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { InfiniteScroll, QuickNav, EntityIcon },
|
components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage },
|
||||||
})
|
})
|
||||||
export default class ProjectsView extends Vue {
|
export default class ProjectsView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -122,8 +125,8 @@ export default class ProjectsView extends Vue {
|
|||||||
if (resp.status === 200 || !resp.data.data) {
|
if (resp.status === 200 || !resp.data.data) {
|
||||||
const plans: ProjectData[] = resp.data.data;
|
const plans: ProjectData[] = resp.data.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, rowid });
|
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Bad server response & data:", resp.status, resp.data);
|
console.log("Bad server response & data:", resp.status, resp.data);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<QuickNav />
|
<QuickNav />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Seed Backup
|
Seed Backup
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<QuickNav />
|
<QuickNav />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
Here is a view of the activity you can see.
|
Here is a view of the activity you can see.
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc outside ml-4">
|
||||||
<li>Each identity and claim has a unique position.</li>
|
<li>Each identity and claim has a unique position.</li>
|
||||||
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
|
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
|
||||||
<li>Each will show at their time of appearance relative to all others.</li>
|
<li>Each will show at their time of appearance relative to all others.</li>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<QuickNav />
|
<QuickNav />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
|
|||||||
@@ -4,62 +4,123 @@ importScripts(
|
|||||||
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
|
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
|
||||||
);
|
);
|
||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
function logConsoleAndDb(message, arg1, arg2) {
|
||||||
console.log("Adding event listener for:", event);
|
// in chrome://serviceworker-internals note that the arg1 and arg2 here will show as "[object Object]" in that page but will show as expandable objects in the console
|
||||||
importScripts(
|
console.log(`${new Date().toISOString()} ${message}`, arg1, arg2);
|
||||||
|
if (self.appendDailyLog) {
|
||||||
|
let fullMessage = `${new Date().toISOString()} ${message}`;
|
||||||
|
if (arg1) {
|
||||||
|
fullMessage += `\n${JSON.stringify(arg1)}`;
|
||||||
|
}
|
||||||
|
if (arg2) {
|
||||||
|
fullMessage += `\n${JSON.stringify(arg2)}`;
|
||||||
|
}
|
||||||
|
self.appendDailyLog(fullMessage);
|
||||||
|
} else {
|
||||||
|
// sometimes we get the error: "Uncaught TypeError: self.appendDailyLog is not a function"
|
||||||
|
console.log(
|
||||||
|
"Not logging to DB (often because self.appendDailyLog doesn't exist).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener("install", async (event) => {
|
||||||
|
console.log("Service worker got install event. Importing scripts...", event);
|
||||||
|
await importScripts(
|
||||||
"safari-notifications.js",
|
"safari-notifications.js",
|
||||||
"nacl.js",
|
"nacl.js",
|
||||||
"noble-curves.js",
|
"noble-curves.js",
|
||||||
"noble-hashes.js",
|
"noble-hashes.js",
|
||||||
);
|
);
|
||||||
|
// this should now be available
|
||||||
|
logConsoleAndDb("Service worker imported all scripts.");
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
logConsoleAndDb("Service worker is activating...", event);
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
|
||||||
|
// and https://web.dev/articles/service-worker-lifecycle#clientsclaim
|
||||||
|
event.waitUntil(clients.claim());
|
||||||
|
logConsoleAndDb("Service worker is activated.");
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("push", function (event) {
|
self.addEventListener("push", function (event) {
|
||||||
|
let text = null;
|
||||||
|
if (event.data) {
|
||||||
|
text = event.data.text();
|
||||||
|
}
|
||||||
|
logConsoleAndDb("Service worker received a push event.", text, event);
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
let payload;
|
let payload;
|
||||||
if (event.data) {
|
if (text) {
|
||||||
payload = JSON.parse(event.data.text());
|
try {
|
||||||
|
payload = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
// don't use payload since it is not JSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a special value that tells the service worker to trigger its daily check.
|
||||||
|
// See https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/c1ed026662e754348a5f91542680bd4f57e5b81e/app.py#L217
|
||||||
|
const DAILY_UPDATE_TITLE = "DAILY_CHECK";
|
||||||
|
|
||||||
|
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
|
||||||
|
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
|
||||||
|
// Make sure it is something other than the DAILY_UPDATE_TITLE.
|
||||||
|
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
|
||||||
|
|
||||||
|
let title;
|
||||||
|
let message = "Got some empty message.";
|
||||||
|
if (payload && payload.title == DIRECT_PUSH_TITLE) {
|
||||||
|
// skip any search logic and show the message directly
|
||||||
|
title = "Direct Notification";
|
||||||
|
message = payload.message || "No details were provided.";
|
||||||
|
} else {
|
||||||
|
// any other title will run through regular filtering logic
|
||||||
|
if (payload && payload.title === DAILY_UPDATE_TITLE) {
|
||||||
|
title = "Daily Update";
|
||||||
|
} else {
|
||||||
|
title = payload.title || "Update";
|
||||||
|
}
|
||||||
|
message = await self.getNotificationCount();
|
||||||
}
|
}
|
||||||
const message = await self.getNotificationCount();
|
|
||||||
if (message) {
|
if (message) {
|
||||||
console.log("Will notify user:", message);
|
|
||||||
const title = payload ? payload.title : "Custom Title";
|
|
||||||
const options = {
|
const options = {
|
||||||
body: message,
|
body: message,
|
||||||
icon: payload ? payload.icon : "icon.png",
|
icon: payload ? payload.icon : "icon.png",
|
||||||
badge: payload ? payload.badge : "badge.png",
|
badge: payload ? payload.badge : "badge.png",
|
||||||
};
|
};
|
||||||
await self.registration.showNotification(title, options);
|
await self.registration.showNotification(title, options);
|
||||||
|
logConsoleAndDb("Notified user:", options);
|
||||||
} else {
|
} else {
|
||||||
console.log("No notification message, so will not tell the user.");
|
logConsoleAndDb("No notification message.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing the push event:", error);
|
logConsoleAndDb("Error with push event", event, error);
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
|
logConsoleAndDb("Service worker got a message...", event);
|
||||||
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
|
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
|
||||||
self.secret = event.data.data;
|
self.secret = event.data.data;
|
||||||
event.ports[0].postMessage({ success: true });
|
event.ports[0].postMessage({ success: true });
|
||||||
}
|
}
|
||||||
});
|
logConsoleAndDb("Service worker posted a message.");
|
||||||
|
|
||||||
self.addEventListener("activate", (event) => {
|
|
||||||
event.waitUntil(clients.claim());
|
|
||||||
console.log("Service worker activated", event);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
console.log("Got fetch event", event.request);
|
logConsoleAndDb("Service worker got fetch event.", event);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("error", (event) => {
|
self.addEventListener("error", (event) => {
|
||||||
console.error("Error in Service Worker:", event.message);
|
logConsoleAndDb("Service worker error", event);
|
||||||
|
console.error("Full Error:", event);
|
||||||
|
console.error("Message:", event.message);
|
||||||
console.error("File:", event.filename);
|
console.error("File:", event.filename);
|
||||||
console.error("Line:", event.lineno);
|
console.error("Line:", event.lineno);
|
||||||
console.error("Column:", event.colno);
|
console.error("Column:", event.colno);
|
||||||
|
|||||||
@@ -395,12 +395,42 @@ async function setMostRecentNotified(id) {
|
|||||||
data["lastNotifiedClaimId"] = id;
|
data["lastNotifiedClaimId"] = id;
|
||||||
await updateRecord(store, data);
|
await updateRecord(store, data);
|
||||||
} else {
|
} else {
|
||||||
console.error("IndexedDB settings record not found.");
|
console.error(
|
||||||
|
"safari-notifications setMostRecentNotified IndexedDB settings record not found",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.oncomplete = () => db.close();
|
transaction.oncomplete = () => db.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("IndexedDB error:", error);
|
console.error(
|
||||||
|
"safari-notifications setMostRecentNotified IndexedDB error",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appendDailyLog(message) {
|
||||||
|
try {
|
||||||
|
const db = await openIndexedDB("TimeSafari");
|
||||||
|
const transaction = db.transaction("logs", "readwrite");
|
||||||
|
const store = transaction.objectStore("logs");
|
||||||
|
// only keep one day's worth of logs
|
||||||
|
const todayKey = new Date().toDateString();
|
||||||
|
const previous = await getRecord(store, todayKey);
|
||||||
|
if (!previous) {
|
||||||
|
await store.clear(); // clear out everything previous when this is today's first log
|
||||||
|
}
|
||||||
|
let fullMessage = (previous && previous.message) || "";
|
||||||
|
if (fullMessage) {
|
||||||
|
fullMessage += "\n";
|
||||||
|
}
|
||||||
|
fullMessage += message;
|
||||||
|
await updateRecord(store, { date: todayKey, message: fullMessage });
|
||||||
|
transaction.oncomplete = () => db.close();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("safari-notifications logMessage IndexedDB error", error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +450,7 @@ function getRecord(store, key) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note that this assumes there is only one record in the store.
|
||||||
function updateRecord(store, data) {
|
function updateRecord(store, data) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = store.put(data);
|
const request = store.put(data);
|
||||||
@@ -430,20 +461,20 @@ function updateRecord(store, data) {
|
|||||||
|
|
||||||
async function fetchAllAccounts() {
|
async function fetchAllAccounts() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let openRequest = indexedDB.open("TimeSafariAccounts");
|
const openRequest = indexedDB.open("TimeSafariAccounts");
|
||||||
|
|
||||||
openRequest.onupgradeneeded = function (event) {
|
openRequest.onupgradeneeded = function (event) {
|
||||||
let db = event.target.result;
|
const db = event.target.result;
|
||||||
if (!db.objectStoreNames.contains("accounts")) {
|
if (!db.objectStoreNames.contains("accounts")) {
|
||||||
db.createObjectStore("accounts", { keyPath: "id" });
|
db.createObjectStore("accounts", { keyPath: "id" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
openRequest.onsuccess = function (event) {
|
openRequest.onsuccess = function (event) {
|
||||||
let db = event.target.result;
|
const db = event.target.result;
|
||||||
let transaction = db.transaction("accounts", "readonly");
|
const transaction = db.transaction("accounts", "readonly");
|
||||||
let objectStore = transaction.objectStore("accounts");
|
const objectStore = transaction.objectStore("accounts");
|
||||||
let getAllRequest = objectStore.getAll();
|
const getAllRequest = objectStore.getAll();
|
||||||
|
|
||||||
getAllRequest.onsuccess = function () {
|
getAllRequest.onsuccess = function () {
|
||||||
resolve(getAllRequest.result);
|
resolve(getAllRequest.result);
|
||||||
@@ -460,78 +491,77 @@ async function fetchAllAccounts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getNotificationCount() {
|
async function getNotificationCount() {
|
||||||
let secret = null;
|
|
||||||
let accounts = [];
|
let accounts = [];
|
||||||
let result = null;
|
let result = null;
|
||||||
if ("secret" in self) {
|
// 1 is our master settings ID; see MASTER_SETTINGS_KEY
|
||||||
secret = self.secret;
|
const settings = await getSettingById(1);
|
||||||
const secretUint8Array = self.decodeBase64(secret);
|
let lastNotifiedClaimId = null;
|
||||||
// 1 is our master settings ID; see MASTER_SETTINGS_KEY
|
if ("lastNotifiedClaimId" in settings) {
|
||||||
const settings = await getSettingById(1);
|
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
|
||||||
let lastNotifiedClaimId = null;
|
}
|
||||||
if ("lastNotifiedClaimId" in settings) {
|
const activeDid = settings["activeDid"];
|
||||||
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
|
accounts = await fetchAllAccounts();
|
||||||
}
|
let activeAccount = null;
|
||||||
const activeDid = settings["activeDid"];
|
for (let i = 0; i < accounts.length; i++) {
|
||||||
accounts = await fetchAllAccounts();
|
if (accounts[i]["did"] == activeDid) {
|
||||||
let did = null;
|
activeAccount = accounts[i];
|
||||||
for (var i = 0; i < accounts.length; i++) {
|
break;
|
||||||
let account = accounts[i];
|
|
||||||
let did = account["did"];
|
|
||||||
if (did == activeDid) {
|
|
||||||
let publicKeyHex = account["publicKeyHex"];
|
|
||||||
let identity = account["identity"];
|
|
||||||
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
|
|
||||||
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
|
|
||||||
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
|
||||||
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
|
|
||||||
|
|
||||||
const msg = decoder.decode(decrypted);
|
|
||||||
const identifier = JSON.parse(JSON.parse(msg));
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
};
|
|
||||||
|
|
||||||
headers["Authorization"] = "Bearer " + (await accessToken(identifier));
|
|
||||||
|
|
||||||
let response = await fetch(
|
|
||||||
settings["apiServer"] + "/api/v2/report/claims",
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: headers,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (response.status == 200) {
|
|
||||||
let json = await response.json();
|
|
||||||
let claims = json["data"];
|
|
||||||
let newClaims = 0;
|
|
||||||
for (var i = 0; i < claims.length; i++) {
|
|
||||||
let claim = claims[i];
|
|
||||||
if (claim["id"] === lastNotifiedClaimId) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
newClaims++;
|
|
||||||
}
|
|
||||||
if (newClaims > 0) {
|
|
||||||
result = `There are ${newClaims} new activities on TimeSafari`;
|
|
||||||
}
|
|
||||||
const most_recent_notified = claims[0]["id"];
|
|
||||||
await setMostRecentNotified(most_recent_notified);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"The service worker got a bad response status when fetching claims:",
|
|
||||||
response.status,
|
|
||||||
response,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const identity = activeAccount && activeAccount["identity"];
|
||||||
|
if (identity && "secret" in self) {
|
||||||
|
const secret = self.secret;
|
||||||
|
const secretUint8Array = self.decodeBase64(secret);
|
||||||
|
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
|
||||||
|
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
|
||||||
|
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
|
||||||
|
const msg = decoder.decode(decrypted);
|
||||||
|
const identifier = JSON.parse(JSON.parse(msg));
|
||||||
|
|
||||||
|
headers["Authorization"] = "Bearer " + (await accessToken(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
settings["apiServer"] + "/api/v2/report/claims",
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (response.status == 200) {
|
||||||
|
const json = await response.json();
|
||||||
|
const claims = json["data"];
|
||||||
|
let newClaims = 0;
|
||||||
|
for (let i = 0; i < claims.length; i++) {
|
||||||
|
const claim = claims[i];
|
||||||
|
if (claim["id"] === lastNotifiedClaimId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newClaims++;
|
||||||
|
}
|
||||||
|
if (newClaims > 0) {
|
||||||
|
result = `There are ${newClaims} new activities on Time Safari`;
|
||||||
|
}
|
||||||
|
const most_recent_notified = claims[0]["id"];
|
||||||
|
await setMostRecentNotified(most_recent_notified);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"safari-notifications getNotificationsCount got a bad response status when fetching claims",
|
||||||
|
response.status,
|
||||||
|
response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.appendDailyLog = appendDailyLog;
|
||||||
self.getNotificationCount = getNotificationCount;
|
self.getNotificationCount = getNotificationCount;
|
||||||
self.decodeBase64 = decodeBase64;
|
self.decodeBase64 = decodeBase64;
|
||||||
|
|||||||
19
web-push.md
@@ -400,3 +400,22 @@ While notifications are turned on, the user can tap on the App Notifications tog
|
|||||||
* Active. (User can change to Muted when the user mutes notifications.)
|
* Active. (User can change to Muted when the user mutes notifications.)
|
||||||
* Muted. (User can change to Active when the user toggles it.)
|
* Muted. (User can change to Active when the user toggles it.)
|
||||||
(Turning mute off automatically after some amount of time is not planned in version 1.)
|
(Turning mute off automatically after some amount of time is not planned in version 1.)
|
||||||
|
|
||||||
|
|
||||||
|
# TROUBLESHOOTING
|
||||||
|
|
||||||
|
## Desktop
|
||||||
|
|
||||||
|
#### Firefox
|
||||||
|
|
||||||
|
Go to `about:debugging` and click on `Inspect` for the service worker.
|
||||||
|
|
||||||
|
#### Chrome
|
||||||
|
|
||||||
|
Go to `chrome://inspect/#service-workers` and click on `Inspect` for the service worker.
|
||||||
|
|
||||||
|
## Mobile
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
|
||||||
|
#### iOS
|
||||||
|
|||||||