Compare commits
120 Commits
service-wo
...
0.1.8
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
|
|
ab523639a5 | ||
|
|
0484dfb253 | ||
|
|
c2839e8a99 | ||
|
|
e533cd3d34 | ||
|
|
18e00b95c7 | ||
|
|
e97cd1b1fa | ||
| ccca93b9f1 | |||
| 1be6c04699 | |||
| 2c33febb0e | |||
| e6f73dc81c | |||
| 0d55a722c5 | |||
| 97ef78f5dd | |||
| 672abac9a9 | |||
| 0607fad3e5 | |||
| 6aa89a1d1d | |||
| 2556d5feb9 | |||
| 3c1654764c | |||
| 4c1e229d62 | |||
| 17444d75de | |||
| f2fb432d2e | |||
| e45689daed | |||
| 041308ebc9 | |||
| 9c36bb509a | |||
| 2c300614ef | |||
| 8849e8806a | |||
| f75094283a | |||
| 0fabccd410 | |||
|
|
8ddf7d9532 | ||
|
|
4078853558 | ||
|
|
f4df5ffa9a | ||
| fa856f7594 | |||
|
|
a60beb483c | ||
| a0db6433a6 | |||
| 59d0772881 | |||
| b18e554886 | |||
| 098ef3c644 | |||
| 6045975b79 | |||
| a6bb036ceb | |||
| 1e2ad85547 | |||
|
|
3e2723b744 | ||
| 4daffe8f40 | |||
| efb1922826 | |||
| c6e10bfdad | |||
| bb122be319 | |||
| 3f436476a2 | |||
| a77d20b572 | |||
| 393d1583ae | |||
| 69a25ddd6c | |||
| a12d7fcc1b | |||
| 69c60e5426 | |||
| 4806acc30e | |||
| 1127d7079b | |||
| 0bbadfec6d | |||
| 276d8b2f19 | |||
| a7fbbbd4cd | |||
| a8d362c14d | |||
| ce5933f645 | |||
| 5cbf917ada | |||
| 7335412145 | |||
| feea1a1d3b | |||
| 7f4d31a79c | |||
| 4041a7d08e | |||
|
|
9846cf3e4c | ||
| 681d949098 | |||
| 3bf8fd0c22 | |||
| fa41fb3415 | |||
| 6dbfc5f77d | |||
| ee6a344daf | |||
| 65a5edf26b |
44
CHANGELOG.md
@@ -9,7 +9,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [0.1.3] - 2023.11
|
||||
## [0.1.8] - 2023.12.27
|
||||
### 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
|
||||
### Added
|
||||
- Web push notifications (though not finalized)
|
||||
- Credentials details page
|
||||
- See more data without an ID
|
||||
- Change units of a give
|
||||
|
||||
|
||||
## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db
|
||||
### Added
|
||||
- Offer on a project
|
||||
### Changed
|
||||
- Automatically set as visible when importing a contact
|
||||
|
||||
|
||||
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
|
||||
### Added
|
||||
- Contact name editing
|
||||
### Changed
|
||||
|
||||
49
README.md
@@ -13,19 +13,34 @@ npm install
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||
|
||||
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit.
|
||||
|
||||
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
|
||||
|
||||
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test".
|
||||
|
||||
* `npm run build`
|
||||
|
||||
* `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/`
|
||||
|
||||
... 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`
|
||||
|
||||
* Revert src/constants/app.ts and/or package.json, edit package.json to increment version & add "-beta", `npm install`, and commit.
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +71,11 @@ Under the "Your Identity" screen, click "Advanced", click "Switch Identity / No
|
||||
|
||||
### Web-push
|
||||
|
||||
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
|
||||
For your own web-push tests, change the push server URL in Advanced settings on the account page, and install Time Safari & push server on the same domain.
|
||||
|
||||
### Icons
|
||||
|
||||
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
|
||||
|
||||
### Manual walk-through
|
||||
|
||||
@@ -84,16 +103,21 @@ For your own web-push tests, change the 'vapid' URL in App.vue, and install apps
|
||||
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
|
||||
|
||||
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
|
||||
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control`
|
||||
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
||||
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
||||
|
||||
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
|
||||
|
||||
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
|
||||
|
||||
### Clear data & restart
|
||||
### Clear/Reset data & restart
|
||||
|
||||
Clear the browser cache for localhost.
|
||||
* 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.)
|
||||
* 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.)
|
||||
|
||||
(If you find more, add them to the HelpNotificationsView.vue file.)
|
||||
|
||||
|
||||
|
||||
@@ -116,3 +140,4 @@ Gifts make the world go 'round!
|
||||
* [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)
|
||||
* [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)
|
||||
|
||||
163
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "crowd-funder-for-time-pwa",
|
||||
"version": "0.1.4",
|
||||
"name": "TimeSafari_Test",
|
||||
"version": "0.1.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "crowd-funder-for-time-pwa",
|
||||
"version": "0.1.4",
|
||||
"name": "TimeSafari_Test",
|
||||
"version": "0.1.8",
|
||||
"dependencies": {
|
||||
"@ethersproject/hdnode": "^5.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
@@ -33,6 +33,7 @@
|
||||
"ethereum-cryptography": "^2.1.2",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"ethr-did-resolver": "^8.1.2",
|
||||
"git-describe": "^4.1.1",
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"localstorage-slim": "^2.5.0",
|
||||
@@ -49,6 +50,7 @@
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"three": "^0.156.1",
|
||||
"util": "^0.12.5",
|
||||
"vue": "^3.3.4",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-facing-decorator": "^3.0.2",
|
||||
@@ -72,13 +74,13 @@
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier": "^3.1.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "~5.2.2"
|
||||
}
|
||||
@@ -2821,9 +2823,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
|
||||
"integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
|
||||
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
@@ -2871,9 +2873,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz",
|
||||
"integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
|
||||
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -5498,12 +5500,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.11",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
||||
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
|
||||
"version": "0.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@humanwhocodes/object-schema": "^1.2.1",
|
||||
"@humanwhocodes/object-schema": "^2.0.1",
|
||||
"debug": "^4.1.1",
|
||||
"minimatch": "^3.0.5"
|
||||
},
|
||||
@@ -5525,9 +5527,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/object-schema": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
|
||||
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
|
||||
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jest/create-cache-key-function": {
|
||||
@@ -8819,11 +8821,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
|
||||
"integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
|
||||
"version": "20.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
|
||||
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.25.1"
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
@@ -8893,8 +8895,7 @@
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz",
|
||||
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw=="
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.2",
|
||||
@@ -9206,6 +9207,12 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@unimodules/core": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@unimodules/core/-/core-7.1.2.tgz",
|
||||
@@ -11245,7 +11252,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
||||
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -12063,7 +12069,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
@@ -14220,18 +14225,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
|
||||
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
|
||||
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.2",
|
||||
"@eslint/js": "8.51.0",
|
||||
"@humanwhocodes/config-array": "^0.11.11",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@eslint/js": "8.55.0",
|
||||
"@humanwhocodes/config-array": "^0.11.13",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
@@ -15891,7 +15897,6 @@
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-callable": "^1.1.3"
|
||||
}
|
||||
@@ -16151,7 +16156,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"devOptional": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -16204,7 +16208,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
@@ -16269,6 +16272,30 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/git-describe": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/git-describe/-/git-describe-4.1.1.tgz",
|
||||
"integrity": "sha512-JC8ganO5kO80G8+XE98TDDjnMXQN3Estk3qdJuG2EGRF/l6zuMTMcN+8OSfQZ5FWpqIRLB015anWX4aSRgnxAQ==",
|
||||
"dependencies": {
|
||||
"@types/semver": "^7.3.8",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"semver": "^5.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/git-describe/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@@ -16355,7 +16382,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
},
|
||||
@@ -16426,7 +16452,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
|
||||
"integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
@@ -16465,7 +16490,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -16477,7 +16501,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -16489,7 +16512,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
||||
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.2"
|
||||
},
|
||||
@@ -17081,6 +17103,21 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
|
||||
@@ -17152,7 +17189,6 @@
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -17257,6 +17293,20 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-generator-function": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
|
||||
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
|
||||
"dependencies": {
|
||||
"has-tostringtag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@@ -17530,7 +17580,6 @@
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
|
||||
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"which-typed-array": "^1.1.11"
|
||||
},
|
||||
@@ -19313,8 +19362,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
@@ -23137,9 +23185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
|
||||
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
||||
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
@@ -26870,9 +26918,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.25.3",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
|
||||
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
"version": "2.0.0",
|
||||
@@ -27055,6 +27103,18 @@
|
||||
"resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz",
|
||||
"integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ=="
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"is-arguments": "^1.0.4",
|
||||
"is-generator-function": "^1.0.7",
|
||||
"is-typed-array": "^1.1.3",
|
||||
"which-typed-array": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -27966,7 +28026,6 @@
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
|
||||
"integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"available-typed-arrays": "^1.0.5",
|
||||
"call-bind": "^1.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "crowd-funder-for-time-pwa",
|
||||
"version": "0.1.4",
|
||||
"name": "TimeSafari_Test",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
@@ -33,6 +33,7 @@
|
||||
"ethereum-cryptography": "^2.1.2",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"ethr-did-resolver": "^8.1.2",
|
||||
"git-describe": "^4.1.1",
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"localstorage-slim": "^2.5.0",
|
||||
@@ -49,6 +50,7 @@
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"three": "^0.156.1",
|
||||
"util": "^0.12.5",
|
||||
"vue": "^3.3.4",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-facing-decorator": "^3.0.2",
|
||||
|
||||
@@ -1,54 +1,25 @@
|
||||
|
||||
tasks:
|
||||
|
||||
- don't show "Give" & "Offer" on project screen if they don't have an identifier
|
||||
- allow some gives even if they aren't registered
|
||||
- 08 notifications :
|
||||
- .2 after turning on notification, don't wait in push server but wait in client for message test
|
||||
- insert tooling (exportable logs?) so that we can see problems and troubleshoot as we onboard
|
||||
- if navigator.serviceWorker is null, then tell the user to wait
|
||||
- Local install works after cleared out cache in Chrome
|
||||
|
||||
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
|
||||
- 40 notifications :
|
||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||
- fix maskable icon
|
||||
|
||||
- .5 allow to manage their notifications even without an identity
|
||||
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
||||
- .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
|
||||
- give users next public key hash
|
||||
|
||||
- .5 Add infinite scroll to gifts on the home page
|
||||
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
||||
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
|
||||
- .1 when creating a plan, select location and then make sure you can deselect on Android
|
||||
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
|
||||
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164 assignee:trent
|
||||
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||
- fix cert generation (since it didn't happen automatically for Nov 30)
|
||||
- .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui
|
||||
|
||||
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
||||
- .5 If notifications are not enabled, add message to front page with link/button to enable
|
||||
|
||||
- .1 Make give description text box into something that expands as they type?
|
||||
- 04 allow user to download claims, mine + ones I can see about me from others
|
||||
- .5 customize favicon assignee-group:ui
|
||||
- .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
|
||||
- .5 Display a more appealing confirmation on the map when erasing the marker
|
||||
- .5 make a VC details page, or link to endorser.ch
|
||||
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
||||
- .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch)
|
||||
- .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
|
||||
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
|
||||
- allow download of each VC (to show that they can actually own their data)
|
||||
|
||||
- contacts v+ :
|
||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||
- .2 show error to user when adding a duplicate contact
|
||||
- 01 parse input more robustly (with CSV lib and not commas)
|
||||
|
||||
- stats v1 :
|
||||
- 01 show numeric stats
|
||||
- 04 show different graphic for projects vs people (gnome?) on world
|
||||
- 01 link to world for specific stats
|
||||
- .5 don't load another instance of a bush if it already exists
|
||||
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
||||
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
||||
- show VC details... somehow:
|
||||
- 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 VCs, mine + ones I can see about me from others
|
||||
- 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)
|
||||
@@ -60,14 +31,57 @@ tasks:
|
||||
- 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
|
||||
|
||||
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
|
||||
- 02 watch for the service worker activation before showing the button to turn on notifications
|
||||
- 01 server - show all claim details when issued by the issuer
|
||||
- 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
|
||||
- .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?
|
||||
- .5 customize favicon assignee-group:ui
|
||||
- .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
|
||||
- .5 Display a more appealing confirmation on the map when erasing the marker
|
||||
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
|
||||
- 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"
|
||||
- .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)
|
||||
|
||||
- contacts v+ :
|
||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||
- .2 show error to user when adding a duplicate contact
|
||||
- 01 parse input more robustly (with CSV lib and not commas)
|
||||
- stats v1 :
|
||||
- 01 show numeric stats
|
||||
- 04 show different graphic for projects vs people (gnome?) on world
|
||||
- 01 link to world for specific stats
|
||||
- .5 don't load another instance of a bush if it already exists
|
||||
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
||||
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
||||
|
||||
- .5 show seed phrase in a QR code for transfer to another device
|
||||
- .5 on DiscoverView, switch to a filter UI (eg. just from friend
|
||||
- .5 don't show "Offer" on project screen if they aren't registered
|
||||
|
||||
- 24 Move to Vite
|
||||
- 32 accept images for projects
|
||||
- 32 accept images for contacts
|
||||
- import project interactions from GitHub/GitLab and manage signing
|
||||
|
||||
- linking between projects or plans :
|
||||
- show total time given to & from a project
|
||||
@@ -90,6 +104,8 @@ tasks:
|
||||
- automated tests, eg. cypress
|
||||
|
||||
- Notifications (wake on the phone, push notifications)
|
||||
- pull instead of push, maybe via scheduled runs
|
||||
- have a notification pop-up on Mac screen
|
||||
|
||||
- Connect with phone contacts
|
||||
|
||||
@@ -102,12 +118,11 @@ tasks:
|
||||
|
||||
- Do we want split first name & last name?
|
||||
|
||||
- 40 notifications v+ :
|
||||
- pull, w/ scheduled runs
|
||||
|
||||
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
||||
- 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:
|
||||
- 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
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
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 |
|
Before Width: | Height: | Size: 22 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 |
186
src/App.vue
@@ -169,20 +169,12 @@
|
||||
>
|
||||
Turn on Notifications
|
||||
</button>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
@click="maybeLater(notification.id)"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
>
|
||||
Maybe Later
|
||||
</button>
|
||||
<button
|
||||
@click="never(notification.id)"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
||||
>
|
||||
Never
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
>
|
||||
Maybe Later
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,6 +230,10 @@
|
||||
</p>
|
||||
|
||||
<button
|
||||
@click="
|
||||
close(notification.id);
|
||||
turnOffNotifications();
|
||||
"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
Turn Off Notifications
|
||||
@@ -261,7 +257,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import axios from "axios";
|
||||
interface ServiceWorkerMessage {
|
||||
type: string;
|
||||
data: string;
|
||||
@@ -285,22 +281,68 @@ interface VapidResponse {
|
||||
};
|
||||
}
|
||||
|
||||
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||
import { db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class App extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
b64 = "";
|
||||
mounted() {
|
||||
axios
|
||||
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid")
|
||||
.then((response: VapidResponse) => {
|
||||
this.b64 = response.data.vapidKey;
|
||||
console.log(this.b64);
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
console.log("New service worker is now controlling the page");
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||
if (settings?.webPushServer) {
|
||||
pushUrl = settings.webPushServer;
|
||||
}
|
||||
|
||||
await axios
|
||||
.get(pushUrl + "/web-push/vapid")
|
||||
.then((response: VapidResponse) => {
|
||||
this.b64 = response.data?.vapidKey || "";
|
||||
console.log("Got vapid key:", this.b64);
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
console.log("New service worker is now controlling the page");
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
console.error("API error", error);
|
||||
});
|
||||
if (!this.b64) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Setting Notifications",
|
||||
text: "Could not set notifications.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.location.host.startsWith("localhost")) {
|
||||
console.log("Ignoring the error getting VAPID for local development.");
|
||||
} else {
|
||||
console.error("Got an error initializing notifications:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Setting Notifications",
|
||||
text: "Got an error setting notifications.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sendMessageToServiceWorker(
|
||||
@@ -328,6 +370,7 @@ export default class App extends Vue {
|
||||
}
|
||||
|
||||
private askPermission(): Promise<NotificationPermission> {
|
||||
console.log("Requesting permission for notifications:", navigator);
|
||||
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
|
||||
return Promise.reject("Service worker not available.");
|
||||
}
|
||||
@@ -368,14 +411,17 @@ export default class App extends Vue {
|
||||
private requestNotificationPermission(): Promise<NotificationPermission> {
|
||||
return Notification.requestPermission().then((permission) => {
|
||||
if (permission !== "granted") {
|
||||
alert("We need notification permission to provide certain features.");
|
||||
alert(
|
||||
"Allow this app permission to make notifications for personal reminders." +
|
||||
" You can adjust them at any time in your settings.",
|
||||
);
|
||||
throw new Error("We weren't granted permission.");
|
||||
}
|
||||
return permission;
|
||||
});
|
||||
}
|
||||
|
||||
async turnOnNotifications() {
|
||||
public async turnOnNotifications() {
|
||||
return this.askPermission()
|
||||
.then((permission) => {
|
||||
console.log("Permission granted:", permission);
|
||||
@@ -384,38 +430,51 @@ export default class App extends Vue {
|
||||
this.subscribeToPush()
|
||||
.then(() => {
|
||||
console.log("Subscribed successfully.");
|
||||
// Assuming the subscription object is available
|
||||
return navigator.serviceWorker.ready;
|
||||
})
|
||||
.then((registration) => {
|
||||
// Fetch the existing subscription object from the registration
|
||||
return registration.pushManager.getSubscription();
|
||||
})
|
||||
.then((subscription) => {
|
||||
if (subscription) {
|
||||
console.log(subscription);
|
||||
return this.sendSubscriptionToServer(subscription);
|
||||
} else {
|
||||
throw new Error("Subscription object is not available.");
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Subscription data sent to server.");
|
||||
console.log(
|
||||
"Subscription data sent to server and all finished successfully.",
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Notifications Turned On",
|
||||
text: "Notifications are on. You should see one on your device; if not, see the 'Troubleshoot' page.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Subscription or server communication failed:",
|
||||
error,
|
||||
);
|
||||
alert(
|
||||
"Subscription or server communication failed. Try again in a while.",
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("An error occurred:", error);
|
||||
// Handle error appropriately here
|
||||
console.error(
|
||||
"An error occurred setting notification permissions:",
|
||||
error,
|
||||
);
|
||||
alert("Some error occurred setting notification permissions.");
|
||||
});
|
||||
}
|
||||
|
||||
// Function to convert URL base64 to Uint8Array
|
||||
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
@@ -459,11 +518,7 @@ export default class App extends Vue {
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Subscription or server communication failed:",
|
||||
error,
|
||||
options,
|
||||
);
|
||||
console.error("Push subscription failed:", error, options);
|
||||
|
||||
// Inform the user about the issue
|
||||
alert(
|
||||
@@ -479,7 +534,7 @@ export default class App extends Vue {
|
||||
private sendSubscriptionToServer(
|
||||
subscription: PushSubscription,
|
||||
): Promise<void> {
|
||||
console.log(subscription);
|
||||
console.log("About to send subscription...", subscription);
|
||||
return fetch("/web-push/subscribe", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -494,12 +549,49 @@ export default class App extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
never(ID: string) {
|
||||
alert(ID);
|
||||
}
|
||||
async turnOffNotifications() {
|
||||
let subscription;
|
||||
const pushProviderSuccess = await navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
return registration.pushManager.getSubscription();
|
||||
})
|
||||
.then((subscript) => {
|
||||
subscription = subscript;
|
||||
if (subscription) {
|
||||
return subscription.unsubscribe();
|
||||
} else {
|
||||
console.log("Subscription object is not available.");
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Push provider server communication failed:", error);
|
||||
return false;
|
||||
});
|
||||
|
||||
maybeLater(ID: string) {
|
||||
alert(ID);
|
||||
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(subscription),
|
||||
})
|
||||
.then((response) => {
|
||||
return response.ok;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Push server communication failed:", error);
|
||||
return false;
|
||||
});
|
||||
|
||||
alert(
|
||||
"Notifications are off. Push provider unsubscribe " +
|
||||
(pushProviderSuccess ? "succeeded" : "failed") +
|
||||
(pushProviderSuccess === pushServerSuccess ? " and" : " but") +
|
||||
" push server unsubscribe " +
|
||||
(pushServerSuccess ? "succeeded" : "failed") +
|
||||
".",
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
BIN
src/assets/help/apple-icon.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/help/chrome-install-pwa.png
Normal file
|
After Width: | Height: | Size: 5.1 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 |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 85 KiB |
@@ -13,18 +13,21 @@
|
||||
<div class="flex flex-row">
|
||||
<span
|
||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2"
|
||||
>Hours</span
|
||||
@click="changeUnitCode()"
|
||||
>
|
||||
{{ UNIT_SHORT[unitCode] }}
|
||||
</span>
|
||||
<div
|
||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="decrement()"
|
||||
v-if="amountInput !== '0'"
|
||||
>
|
||||
<fa icon="chevron-left" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
||||
v-model="hours"
|
||||
v-model="amountInput"
|
||||
/>
|
||||
<div
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@@ -81,12 +84,31 @@ export default class GiftedDialog extends Vue {
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
|
||||
amountInput = "0";
|
||||
giver?: GiverInputInfo; // undefined means no identified giver agent
|
||||
description = "";
|
||||
givenToUser = false;
|
||||
hours = "0";
|
||||
unitCode = "HUR";
|
||||
visible = false;
|
||||
|
||||
/* eslint-disable prettier/prettier */
|
||||
UNIT_SHORT: Record<string, string> = {
|
||||
"BTC": "BTC",
|
||||
"ETH": "ETH",
|
||||
"HUR": "Hours",
|
||||
"USD": "US $",
|
||||
};
|
||||
/* eslint-enable prettier/prettier */
|
||||
|
||||
/* eslint-disable prettier/prettier */
|
||||
UNIT_LONG: Record<string, string> = {
|
||||
"BTC": "BTC",
|
||||
"ETH": "ETH",
|
||||
"HUR": "hours",
|
||||
"USD": "dollars",
|
||||
};
|
||||
/* eslint-enable prettier/prettier */
|
||||
|
||||
async created() {
|
||||
try {
|
||||
await db.open();
|
||||
@@ -101,9 +123,7 @@ export default class GiftedDialog extends Vue {
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
err.message ||
|
||||
"There was an error retrieving the latest sweet, sweet action.",
|
||||
text: err.message || "There was an error retrieving your settings.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -115,7 +135,7 @@ export default class GiftedDialog extends Vue {
|
||||
this.giver = giver;
|
||||
// if we show "given to user" selection, default checkbox to true
|
||||
this.givenToUser = this.showGivenToUser;
|
||||
this.hours = "0";
|
||||
this.amountInput = "0";
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
@@ -125,12 +145,21 @@ export default class GiftedDialog extends Vue {
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
changeUnitCode() {
|
||||
const units = Object.keys(this.UNIT_SHORT);
|
||||
const index = units.indexOf(this.unitCode);
|
||||
this.unitCode = units[(index + 1) % units.length];
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||
}
|
||||
|
||||
decrement() {
|
||||
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||
this.amountInput = `${Math.max(
|
||||
0,
|
||||
(parseFloat(this.amountInput) || 1) - 1,
|
||||
)}`;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
@@ -142,7 +171,7 @@ export default class GiftedDialog extends Vue {
|
||||
this.description = "";
|
||||
this.giver = undefined;
|
||||
this.givenToUser = this.showGivenToUser;
|
||||
this.hours = "0";
|
||||
this.amountInput = "0";
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
@@ -160,7 +189,8 @@ export default class GiftedDialog extends Vue {
|
||||
await this.recordGive(
|
||||
this.giver?.did as string | undefined,
|
||||
this.description,
|
||||
parseFloat(this.hours),
|
||||
parseFloat(this.amountInput),
|
||||
this.unitCode,
|
||||
).then(() => {
|
||||
this.eraseValues();
|
||||
});
|
||||
@@ -186,12 +216,13 @@ export default class GiftedDialog extends Vue {
|
||||
*
|
||||
* @param giverDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
* @param amountInput may be 0
|
||||
*/
|
||||
public async recordGive(
|
||||
giverDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
amountInput?: number,
|
||||
unitCode?: string,
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
@@ -206,13 +237,15 @@ export default class GiftedDialog extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description && !hours) {
|
||||
if (!description && !amountInput) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must enter a description or some number of hours.",
|
||||
text: `You must enter a description or some number of ${
|
||||
this.UNIT_LONG[this.unitCode]
|
||||
}.`,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -228,7 +261,8 @@ export default class GiftedDialog extends Vue {
|
||||
giverDid,
|
||||
this.givenToUser ? this.activeDid : undefined,
|
||||
description,
|
||||
hours,
|
||||
amountInput,
|
||||
unitCode,
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
|
||||
@@ -105,9 +105,7 @@ export default class OfferDialog extends Vue {
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
err.message ||
|
||||
"There was an error retrieving the latest sweet, sweet action.",
|
||||
text: err.message || "There was an error retrieving your settings.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
|
||||
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,20 +4,26 @@
|
||||
* See also ../libs/veramo/setup.ts
|
||||
*/
|
||||
export enum AppString {
|
||||
APP_NAME = "TimeSafari",
|
||||
|
||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||
|
||||
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
||||
PROD_PUSH_SERVER = "https://timesafari.app",
|
||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
||||
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
||||
}
|
||||
|
||||
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
|
||||
|
||||
export const DEFAULT_PUSH_SERVER =
|
||||
window.location.protocol + "//" + window.location.host;
|
||||
|
||||
/**
|
||||
* See notiwind package
|
||||
* The possible values for "group" and "type" are in App.vue.
|
||||
* From the notiwind package
|
||||
*/
|
||||
export interface NotificationIface {
|
||||
group: string;
|
||||
group: string; // "alert" | "modal"
|
||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
||||
title: string;
|
||||
text: string;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import BaseDexie, { Table } from "dexie";
|
||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||
import { Account, AccountsSchema } from "./tables/accounts";
|
||||
import { Contact, ContactsSchema } from "./tables/contacts";
|
||||
import { Contact, ContactSchema } from "./tables/contacts";
|
||||
import { Log, LogSchema } from "./tables/logs";
|
||||
import {
|
||||
MASTER_SETTINGS_KEY,
|
||||
Settings,
|
||||
SettingsSchema,
|
||||
} 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
|
||||
type SensitiveTables = { accounts: Table<Account> };
|
||||
type NonsensitiveTables = {
|
||||
contacts: Table<Contact>;
|
||||
logs: Table<Log>;
|
||||
settings: Table<Settings>;
|
||||
};
|
||||
|
||||
@@ -26,7 +28,11 @@ export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||
const SensitiveSchemas = { ...AccountsSchema };
|
||||
|
||||
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.
|
||||
const secret =
|
||||
@@ -38,12 +44,14 @@ encrypted(accountsDB, { secretKey: secret });
|
||||
|
||||
// Define the schema for our databases
|
||||
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
|
||||
db.on("populate", () => {
|
||||
db.settings.add({
|
||||
id: MASTER_SETTINGS_KEY,
|
||||
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
||||
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,6 @@ export interface Contact {
|
||||
registered?: boolean;
|
||||
}
|
||||
|
||||
export const ContactsSchema = {
|
||||
export const ContactSchema = {
|
||||
contacts: "&did, name, publicKeyBase64, registered, seesMe",
|
||||
};
|
||||
|
||||
10
src/db/tables/logs.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Log {
|
||||
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, message",
|
||||
};
|
||||
@@ -12,17 +12,19 @@ export type BoundingBox = {
|
||||
* Settings type encompasses user-specific configuration details.
|
||||
*/
|
||||
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
|
||||
apiServer?: string; // API server URL
|
||||
firstName?: string; // User's first name
|
||||
lastName?: string; // User's last name
|
||||
lastViewedClaimId?: string; // Last viewed claim ID
|
||||
lastNotifiedClaimId?: string; // Last notified claim ID
|
||||
isRegistered?: boolean;
|
||||
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
|
||||
|
||||
searchBoxes?: Array<{
|
||||
name: string;
|
||||
bbox: BoundingBox;
|
||||
@@ -30,8 +32,9 @@ export type Settings = {
|
||||
|
||||
showContactGivesInline?: boolean; // Display contact inline or not
|
||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
||||
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
||||
reminderOn?: boolean; // Toggle to enable or disable reminders
|
||||
warnIfProdServer?: boolean; // Warn if using a production server
|
||||
warnIfTestServer?: boolean; // Warn if using a testing server
|
||||
webPushServer?: string; // Web Push server URL
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,8 @@ export const SERVICE_ID = "endorser.ch";
|
||||
export const CONTACT_URL_PREFIX = "https://endorser.ch";
|
||||
// the suffix for the contact URL
|
||||
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
|
||||
// the prefix for handle IDs, the permanent ID for claims on Endorser
|
||||
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
||||
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
@@ -38,14 +40,24 @@ export interface ClaimResult {
|
||||
error: { code: string; message: string };
|
||||
}
|
||||
|
||||
export interface GenericClaim {
|
||||
export interface GenericVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
issuedAt: string;
|
||||
// "any" because arbitrary objects can be subject of agreement
|
||||
}
|
||||
|
||||
export interface GenericServerRecord extends GenericVerifiableCredential {
|
||||
handleId?: string;
|
||||
id?: string;
|
||||
issuedAt?: string;
|
||||
issuer?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
claim: Record<any, any>;
|
||||
}
|
||||
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
"@type": "",
|
||||
claim: {},
|
||||
};
|
||||
|
||||
export interface GiveServerRecord {
|
||||
agentDid: string;
|
||||
@@ -69,6 +81,8 @@ export interface OfferServerRecord {
|
||||
validThrough: string;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id4
|
||||
export interface GiveVerifiableCredential {
|
||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||
"@type": "GiveAction";
|
||||
@@ -80,6 +94,8 @@ export interface GiveVerifiableCredential {
|
||||
recipient?: { identifier: string };
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id8
|
||||
export interface OfferVerifiableCredential {
|
||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||
"@type": "Offer";
|
||||
@@ -93,6 +109,8 @@ export interface OfferVerifiableCredential {
|
||||
validThrough?: string;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id7
|
||||
export interface PlanVerifiableCredential {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "PlanAction";
|
||||
@@ -137,6 +155,104 @@ export function isHiddenDid(did: string) {
|
||||
return did === HIDDEN_DID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true for any nested string where func(input) === true
|
||||
*
|
||||
* Similar logic is found in endorser-mobile.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function testRecursivelyOnString(func: (arg0: any) => boolean, input: any) {
|
||||
if (Object.prototype.toString.call(input) === "[object String]") {
|
||||
return func(input);
|
||||
} else if (input instanceof Object) {
|
||||
if (!Array.isArray(input)) {
|
||||
// it's an object
|
||||
for (const key in input) {
|
||||
if (testRecursivelyOnString(func, input[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// it's an array
|
||||
for (const value of input) {
|
||||
if (testRecursivelyOnString(func, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function containsHiddenDid(obj: any) {
|
||||
return testRecursivelyOnString(isHiddenDid, obj);
|
||||
}
|
||||
|
||||
export function stripEndorserPrefix(claimId: string) {
|
||||
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
|
||||
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
|
||||
} else {
|
||||
return claimId;
|
||||
}
|
||||
}
|
||||
|
||||
// similar logic is found in endorser-mobile
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function removeSchemaContext(obj: any) {
|
||||
return obj["@context"] === SCHEMA_ORG_CONTEXT
|
||||
? R.omit(["@context"], obj)
|
||||
: obj;
|
||||
}
|
||||
|
||||
// similar logic is found in endorser-mobile
|
||||
export function addLastClaimOrHandleAsIdIfMissing(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
obj: any,
|
||||
lastClaimId?: string,
|
||||
handleId?: string,
|
||||
) {
|
||||
if (!obj.identifier && lastClaimId) {
|
||||
const result = R.clone(obj);
|
||||
result.lastClaimId = lastClaimId;
|
||||
return result;
|
||||
} else if (!obj.identifier && handleId) {
|
||||
const result = R.clone(obj);
|
||||
result.identifier = handleId;
|
||||
return result;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
// return clone of object without any nested *VisibleToDids keys
|
||||
// similar logic is found in endorser-mobile
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function removeVisibleToDids(input: any): any {
|
||||
if (input instanceof Object) {
|
||||
if (!Array.isArray(input)) {
|
||||
// it's an object
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in input) {
|
||||
if (!key.endsWith("VisibleToDids")) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
result[key] = removeVisibleToDids(R.clone(input[key]));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
// it's an array
|
||||
return R.map(removeVisibleToDids, input);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
||||
|
||||
@@ -157,8 +273,8 @@ export function didInfo(
|
||||
return contact
|
||||
? contact.name || "Contact With No Name"
|
||||
: isHiddenDid(did)
|
||||
? "Someone Not In Network"
|
||||
: "Someone Not In Contacts";
|
||||
? "Someone Not In Network"
|
||||
: "Someone Not In Contacts";
|
||||
}
|
||||
|
||||
export interface ResultWithType {
|
||||
@@ -194,6 +310,7 @@ export async function createAndSubmitGive(
|
||||
toDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
unitCode?: string,
|
||||
fulfillsProjectHandleId?: string,
|
||||
): Promise<CreateAndSubmitClaimResult> {
|
||||
const vcClaim: GiveVerifiableCredential = {
|
||||
@@ -202,13 +319,15 @@ export async function createAndSubmitGive(
|
||||
recipient: toDid ? { identifier: toDid } : undefined,
|
||||
agent: fromDid ? { identifier: fromDid } : undefined,
|
||||
description: description || undefined,
|
||||
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
|
||||
object: hours
|
||||
? { amountOfThisGood: hours, unitCode: unitCode || "HUR" }
|
||||
: undefined,
|
||||
fulfills: fulfillsProjectHandleId
|
||||
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
||||
: undefined,
|
||||
};
|
||||
return createAndSubmitClaim(
|
||||
vcClaim as GenericClaim,
|
||||
vcClaim as GenericServerRecord,
|
||||
identity,
|
||||
apiServer,
|
||||
axios,
|
||||
@@ -256,7 +375,7 @@ export async function createAndSubmitOffer(
|
||||
};
|
||||
}
|
||||
return createAndSubmitClaim(
|
||||
vcClaim as GenericClaim,
|
||||
vcClaim as GenericServerRecord,
|
||||
identity,
|
||||
apiServer,
|
||||
axios,
|
||||
@@ -264,7 +383,7 @@ export async function createAndSubmitOffer(
|
||||
}
|
||||
|
||||
export async function createAndSubmitClaim(
|
||||
vcClaim: GenericClaim,
|
||||
vcClaim: GenericVerifiableCredential,
|
||||
identity: IIdentifier,
|
||||
apiServer: string,
|
||||
axios: Axios,
|
||||
@@ -367,6 +486,10 @@ export interface ProjectData {
|
||||
* URL referencing information about the project
|
||||
**/
|
||||
handleId: string;
|
||||
/**
|
||||
* The DID of the issuer
|
||||
*/
|
||||
issuerDid: string;
|
||||
/**
|
||||
* The Identier of the project
|
||||
**/
|
||||
|
||||
5
src/libs/util.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// many of these are also found in endorser-mobile utility.ts
|
||||
|
||||
export const isGlobalUri = (uri: string) => {
|
||||
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
||||
};
|
||||
12
src/main.ts
@@ -13,7 +13,9 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowUpRightFromSquare,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faChevronLeft,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsisVertical,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
@@ -34,16 +37,19 @@ import {
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHand,
|
||||
faHouseChimney,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faQrcode,
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
@@ -60,7 +66,9 @@ import {
|
||||
library.add(
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowUpRightFromSquare,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faChevronLeft,
|
||||
@@ -74,6 +82,7 @@ library.add(
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsisVertical,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
@@ -81,17 +90,20 @@ library.add(
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHand,
|
||||
faHouseChimney,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQrcode,
|
||||
faQuestion,
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
|
||||
@@ -39,7 +39,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "account",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
|
||||
beforeEnter: enterOrStart,
|
||||
},
|
||||
{
|
||||
path: "/claim/:id?",
|
||||
name: "claim",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "claim" */ "../views/ClaimView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/confirm-contact",
|
||||
@@ -91,6 +96,14 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/help-notifications",
|
||||
name: "help-notifications",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "help-notifications" */ "../views/HelpNotificationsView.vue"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/identity-switcher",
|
||||
name: "identity-switcher",
|
||||
|
||||
2184
src/util.d.ts
vendored
Normal file
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<QuickNav selected="Profile"></QuickNav>
|
||||
<TopMessage />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Heading -->
|
||||
@@ -32,8 +34,24 @@
|
||||
</span>
|
||||
</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 -->
|
||||
<!-- 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
|
||||
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"
|
||||
@@ -58,9 +76,9 @@
|
||||
<span v-else>
|
||||
<router-link
|
||||
:to="{ name: 'new-edit-account' }"
|
||||
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
||||
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"
|
||||
>
|
||||
(set name)
|
||||
(Set Your Name)
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
@@ -87,54 +105,20 @@
|
||||
</router-link>
|
||||
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||
<label
|
||||
for="toggleNotifications"
|
||||
class="flex items-center cursor-pointer"
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-permission',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
<div
|
||||
v-if="!notificationMaybeChanged"
|
||||
class="flex items-center justify-between cursor-pointer"
|
||||
@click="showNotificationChoice()"
|
||||
>
|
||||
<!-- label -->
|
||||
<div>App Notifications</div>
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input type="checkbox" name="toggleNotifications" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
for="toggleMuteNotifications"
|
||||
class="flex items-center cursor-pointer mt-4"
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-mute',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- label -->
|
||||
<div>Mute Notifications</div>
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
name="toggleMuteNotifications"
|
||||
v-model="isSubscribed"
|
||||
name="toggleNotificationsInput"
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- line -->
|
||||
@@ -144,14 +128,21 @@
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else>
|
||||
Notification status may have changed. Revisit this page to see the
|
||||
latest setting.
|
||||
</div>
|
||||
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
|
||||
Test your notification setup.
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||
|
||||
<router-link
|
||||
:to="{ name: 'seed-backup' }"
|
||||
href=""
|
||||
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"
|
||||
>
|
||||
Backup Identifier Seed
|
||||
@@ -166,30 +157,42 @@
|
||||
</a>
|
||||
<a ref="downloadLink" />
|
||||
|
||||
<div v-if="activeDid" class="flex py-2">
|
||||
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
||||
<div v-if="activeDid" class="my-8">
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">Rate Limits</h3>
|
||||
<button
|
||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
@click="checkLimits()"
|
||||
>
|
||||
Check Limits
|
||||
</button>
|
||||
<!-- show spinner if loading limits -->
|
||||
<div v-if="loadingLimits" class="ml-2">
|
||||
Checking... <fa icon="spinner" class="fa-spin"></fa>
|
||||
<div v-if="loadingLimits" class="text-center mb-4">
|
||||
Checking… <fa icon="spinner" class="fa-spin"></fa>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<div class="mb-4">
|
||||
{{ limitsMessage }}
|
||||
</div>
|
||||
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
||||
<span class="font-bold">Rate Limits</span>
|
||||
<p>
|
||||
You have done {{ limits.doneClaimsThisWeek }} claims out of
|
||||
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
|
||||
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
|
||||
<div v-if="!!limits?.nextWeekBeginDateTime">
|
||||
<p class="mb-3 text-sm">
|
||||
You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of
|
||||
<b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims
|
||||
counter resets at
|
||||
<b class="whitespace-nowrap">{{
|
||||
readableTime(limits.nextWeekBeginDateTime)
|
||||
}}</b>
|
||||
</p>
|
||||
<p>
|
||||
You have done {{ limits.doneRegistrationsThisMonth }} registrations
|
||||
out of {{ limits.maxRegistrationsPerMonth }} for this month. (You can
|
||||
register nobody on your first day, and after that only one a day in
|
||||
your first month.) Your registration counter resets at
|
||||
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
||||
<p class="text-sm">
|
||||
You have done
|
||||
<b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of
|
||||
<b>{{ limits.maxRegistrationsPerMonth }}</b> for this month.
|
||||
<i
|
||||
>(You can register nobody on your first day, and after that only one
|
||||
a day in your first month.)</i
|
||||
>
|
||||
Your registration counter resets at
|
||||
<b class="whitespace-nowrap">
|
||||
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,10 +205,14 @@
|
||||
>
|
||||
Advanced
|
||||
</h3>
|
||||
|
||||
<div v-if="showAdvanced">
|
||||
<p class="text-rose-600 mb-8">
|
||||
Beware: the features here can be confusing and even change data in ways
|
||||
you do not expect. But we support your freedom!
|
||||
</p>
|
||||
|
||||
<!-- Deep Identity Details -->
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
|
||||
<h2 class="text-sm uppercase font-semibold mb-3">
|
||||
Deep Identity Details
|
||||
</h2>
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
@@ -263,13 +270,11 @@
|
||||
|
||||
<label
|
||||
for="toggleShowAmounts"
|
||||
class="flex items-center cursor-pointer py-2"
|
||||
class="flex items-center justify-between cursor-pointer my-4"
|
||||
@click="handleChange"
|
||||
>
|
||||
<!-- label -->
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||
Show amounts given with contacts
|
||||
</h2>
|
||||
<h2>Show amounts given with contacts</h2>
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
@@ -288,26 +293,21 @@
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="flex py-2">
|
||||
<button class="text-blue-500">
|
||||
<!-- id used by puppeteer test script -->
|
||||
<router-link
|
||||
id="switch-identity-link"
|
||||
:to="{ name: 'identity-switcher' }"
|
||||
class="block text-center"
|
||||
>
|
||||
Switch Identity / No Identity
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ name: 'statistics' }"
|
||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
See Animated Global History of Giving
|
||||
</router-link>
|
||||
|
||||
<div class="flex py-2">
|
||||
<button class="text-blue-500">
|
||||
<router-link :to="{ name: 'statistics' }" class="block text-center">
|
||||
See Achievements & Statistics
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
<!-- id used by puppeteer test script -->
|
||||
<router-link
|
||||
id="switch-identity-link"
|
||||
: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"
|
||||
>
|
||||
Switch Identity
|
||||
</router-link>
|
||||
|
||||
<div class="flex py-4">
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
||||
@@ -324,35 +324,117 @@
|
||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||
</button>
|
||||
<button
|
||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)"
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
||||
>
|
||||
Use Prod
|
||||
</button>
|
||||
<button
|
||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)"
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
||||
>
|
||||
Use Test
|
||||
</button>
|
||||
<button
|
||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)"
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
||||
>
|
||||
Use Local
|
||||
</button>
|
||||
</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">
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||
Notification Push Server
|
||||
</h2>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
v-model="webPushServerInput"
|
||||
/>
|
||||
<button
|
||||
v-if="webPushServerInput != webPushServer"
|
||||
class="px-4 rounded bg-red-500 border border-slate-400"
|
||||
@click="onClickSavePushServer()"
|
||||
>
|
||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
|
||||
>
|
||||
Use Prod
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
|
||||
>
|
||||
Use Test 1
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
|
||||
>
|
||||
Use Test 2
|
||||
</button>
|
||||
</div>
|
||||
<span class="px-4 text-sm" v-if="!webPushServerInput">
|
||||
When that setting is blank, this app will use the default web push
|
||||
server URL:
|
||||
{{ AppConstants.DEFAULT_PUSH_SERVER }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosError } from "axios";
|
||||
import { AxiosError, AxiosRequestConfig } from "axios";
|
||||
import "dexie-export-import";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import TopMessage from "@/components/TopMessage.vue";
|
||||
import { AppString } from "@/constants/app";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
@@ -377,11 +459,11 @@ interface IAccount {
|
||||
derivationPath: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
@Component({ components: { QuickNav, TopMessage } })
|
||||
export default class AccountViewView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
Constants = AppString;
|
||||
AppConstants = AppString;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
@@ -389,9 +471,13 @@ export default class AccountViewView extends Vue {
|
||||
derivationPath = "";
|
||||
givenName = "";
|
||||
isRegistered = false;
|
||||
isSubscribed = false;
|
||||
notificationMaybeChanged = false;
|
||||
numAccounts = 0;
|
||||
publicHex = "";
|
||||
publicBase64 = "";
|
||||
webPushServer = "";
|
||||
webPushServerInput = "";
|
||||
limits: RateLimits | null = null;
|
||||
limitsMessage = "";
|
||||
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
|
||||
@@ -404,6 +490,65 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
showAdvanced = false;
|
||||
|
||||
subscription: PushSubscription | null = null;
|
||||
warnIfProdServer = false;
|
||||
warnIfTestServer = false;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
try {
|
||||
// Open the accounts database
|
||||
@@ -469,6 +614,16 @@ export default class AccountViewView extends Vue {
|
||||
this.updateShowContactAmounts();
|
||||
}
|
||||
|
||||
toggleProdWarning() {
|
||||
this.warnIfProdServer = !this.warnIfProdServer;
|
||||
this.updateWarnIfProdServer(this.warnIfProdServer);
|
||||
}
|
||||
|
||||
toggleTestWarning() {
|
||||
this.warnIfTestServer = !this.warnIfTestServer;
|
||||
this.updateWarnIfTestServer(this.warnIfTestServer);
|
||||
}
|
||||
|
||||
readableTime(timeStr: string) {
|
||||
return timeStr.substring(0, timeStr.indexOf("T"));
|
||||
}
|
||||
@@ -478,47 +633,6 @@ export default class AccountViewView extends Vue {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes component state with values from the database or defaults.
|
||||
* @param {SettingsType} settings - Object containing settings from the database.
|
||||
*/
|
||||
initializeState(settings: Settings | undefined) {
|
||||
this.activeDid = (settings?.activeDid as string) || "";
|
||||
this.apiServer = (settings?.apiServer as string) || "";
|
||||
this.apiServerInput = (settings?.apiServer as string) || "";
|
||||
this.givenName =
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.showContactGives = !!settings?.showContactGivesInline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the identity and updates the component's state.
|
||||
* @param {IdentityType} identity - Object containing identity information.
|
||||
@@ -543,6 +657,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.
|
||||
* @param {Error} err - The error object.
|
||||
@@ -581,12 +720,58 @@ export default class AccountViewView extends Vue {
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
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,
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -711,7 +896,7 @@ export default class AccountViewView extends Vue {
|
||||
});
|
||||
this.isRegistered = true;
|
||||
} catch (err) {
|
||||
console.log("Got an error updating settings:", err);
|
||||
console.error("Got an error updating settings:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -740,7 +925,7 @@ export default class AccountViewView extends Vue {
|
||||
private async fetchRateLimits(identity: IIdentifier) {
|
||||
const url = `${this.apiServer}/api/report/rateLimits`;
|
||||
const headers = await this.getHeaders(identity);
|
||||
return await this.axios.get(url, { headers });
|
||||
return await this.axios.get(url, { headers } as AxiosRequestConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -753,10 +938,9 @@ export default class AccountViewView extends Vue {
|
||||
const data = error.response?.data as ErrorResponse;
|
||||
this.limitsMessage =
|
||||
(data?.error?.message as string) || "Bad server response.";
|
||||
console.log(
|
||||
console.error(
|
||||
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
|
||||
this.limitsMessage,
|
||||
//error,
|
||||
);
|
||||
} else if (
|
||||
error instanceof Error &&
|
||||
@@ -844,8 +1028,21 @@ export default class AccountViewView extends Vue {
|
||||
this.apiServer = this.apiServerInput;
|
||||
}
|
||||
|
||||
setApiServerInput(value: string) {
|
||||
this.apiServerInput = value;
|
||||
async onClickSavePushServer() {
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
webPushServer: this.webPushServerInput,
|
||||
});
|
||||
this.webPushServer = this.webPushServerInput;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Reload",
|
||||
text: "Now reload the app to get a new VAPID to use with this push server.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
476
src/views/ClaimView.vue
Normal file
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<QuickNav />
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Breadcrumb -->
|
||||
<div id="ViewBreadcrumb" class="mb-8">
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
</button>
|
||||
Verifiable Claim Details
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Details -->
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<div>
|
||||
<div class="block flex gap-4 overflow-hidden">
|
||||
<div class="overflow-hidden">
|
||||
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
|
||||
<div class="text-sm">
|
||||
<div>
|
||||
{{ veriClaim.claimType }}
|
||||
</div>
|
||||
<div>
|
||||
<fa icon="message" class="fa-fw text-slate-400"></fa>
|
||||
{{ veriClaim.claim?.description }}
|
||||
</div>
|
||||
<div>
|
||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
{{ veriClaim.issuer }}
|
||||
</div>
|
||||
<div>
|
||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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-else-if="totalConfirmers() === 1">
|
||||
One person has confirmed this.
|
||||
</span>
|
||||
<span v-else> {{ totalConfirmers() }} people have confirmed this. </span>
|
||||
|
||||
<div v-if="totalConfirmers() > 0">
|
||||
<div
|
||||
v-if="
|
||||
confirmerIdList.length === 0 && confsVisibleToIdList.length === 0
|
||||
"
|
||||
>
|
||||
Nobody that you know confirmed this claim, nor do they have any
|
||||
confirmers in their network.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="confirmerIdList.length === 0 && confsVisibleToIdList.length > 0"
|
||||
>
|
||||
<!-- Only show if this person has links to confirmers (below). -->
|
||||
Nobody that you know has issued or confirmed this claim.
|
||||
</div>
|
||||
<div v-if="confirmerIdList.length > 0">
|
||||
The following people have issued or confirmed this claim.
|
||||
<ul>
|
||||
<li
|
||||
v-for="confirmerId in confirmerIdList"
|
||||
:key="confirmerId"
|
||||
class="list-disc"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="grow overflow-hidden">
|
||||
<div class="text-sm">
|
||||
{{ confirmerId }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Never need to show the following message.
|
||||
If there is nobody in the confirmerIdList then we'll show the combined "nobody" message.
|
||||
If there is somebody in the confirmerIdList then that's all they need to show.
|
||||
-->
|
||||
<!-- Nobody that you know can see someone who has confirmed this claim. -->
|
||||
|
||||
<div v-if="confsVisibleToIdList.length > 0">
|
||||
The following people can connect you with people who have issued or
|
||||
confirmed this claim.
|
||||
<ul>
|
||||
<li
|
||||
v-for="confsVisibleTo in confsVisibleToIdList"
|
||||
:key="confsVisibleTo"
|
||||
class="list-disc"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="grow overflow-hidden">
|
||||
<div class="text-sm">
|
||||
{{ confsVisibleTo }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div v-if="confirmerIdList.includes(activeDid)">
|
||||
You have confirmed this claim.
|
||||
</div>
|
||||
<div v-else-if="containsHiddenDid(veriClaim.claim)">
|
||||
You cannot confirm this claim because it contains data that is hidden
|
||||
from you.
|
||||
</div>
|
||||
<div v-else>
|
||||
<button
|
||||
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
|
||||
@click="confirmClaim(veriClaim.id)"
|
||||
>
|
||||
Confirm Claim
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2>
|
||||
<pre class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md">
|
||||
{{ util.inspect(veriClaim, false, null) }}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2>
|
||||
<p class="mb-4">
|
||||
The full claim includes the claim as it was originally issued, including
|
||||
the signature (ie. the proof of issuance by that person).
|
||||
</p>
|
||||
<div v-if="!fullClaim">
|
||||
<p v-if="fullClaimMessage" class="mb-4">
|
||||
{{ fullClaimMessage }}
|
||||
</p>
|
||||
<button
|
||||
v-else
|
||||
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)"
|
||||
>
|
||||
Load Full Claim Details
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre>{{ util.inspect(fullClaim, false, null) }}</pre>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="apiServer + '/api/claim/' + veriClaim.id"
|
||||
target="_blank"
|
||||
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>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
import * as R from "ramda";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import * as util from "util";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import OfferDialog from "@/components/OfferDialog.vue";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import * as serverUtil from "@/libs/endorserServer";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
|
||||
})
|
||||
export default class ClaimView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
allMyDids: Array<string> = [];
|
||||
allContacts: Array<Contact> = [];
|
||||
apiServer = "";
|
||||
confirmerIdList = []; // list of DIDs that have confirmed this claim excluding the issuer
|
||||
confsVisibleErrorMessage = "";
|
||||
confsVisibleToIdList = []; // list of DIDs that can see any confirmer
|
||||
fullClaim = null;
|
||||
fullClaimMessage = "";
|
||||
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
|
||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
|
||||
util = util;
|
||||
containsHiddenDid = serverUtil.containsHiddenDid;
|
||||
|
||||
async created() {
|
||||
await db.open();
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
|
||||
await accountsDB.open();
|
||||
const accounts = accountsDB.accounts;
|
||||
const accountsArr = await accounts?.toArray();
|
||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
const pathParam = window.location.pathname.substring("/claim/".length);
|
||||
let claimId;
|
||||
if (pathParam) {
|
||||
claimId = decodeURIComponent(pathParam);
|
||||
this.loadClaim(claimId, identity);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "No claim ID was provided.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
totalConfirmers() {
|
||||
return (
|
||||
this.numConfsNotVisible +
|
||||
this.confirmerIdList.length +
|
||||
this.confsVisibleToIdList.length
|
||||
);
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid: string): Promise<IIdentifier> {
|
||||
await accountsDB.open();
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
throw new Error(
|
||||
"Attempted to load project records with no identity available.",
|
||||
);
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const headers: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (identity) {
|
||||
const token = await accessToken(identity);
|
||||
headers["Authorization"] = "Bearer " + token;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Isn't there a better way to make this available to the template?
|
||||
didInfo(
|
||||
did: string,
|
||||
activeDid: string,
|
||||
dids: Array<string>,
|
||||
contacts: Array<Contact>,
|
||||
) {
|
||||
return serverUtil.didInfo(did, activeDid, dids, contacts);
|
||||
}
|
||||
|
||||
async loadClaim(claimId: string, identity: IIdentifier) {
|
||||
const url =
|
||||
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(claimId);
|
||||
const headers = await this.getHeaders(identity);
|
||||
|
||||
try {
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
if (resp.status === 200) {
|
||||
this.veriClaim = resp.data;
|
||||
} else {
|
||||
// actually, axios typically throws an error so we never get here
|
||||
console.log("Error getting claim:", resp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was a problem getting that claim. See logs for more info.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const serverError = error as AxiosError;
|
||||
console.error("Error retrieving claim:", serverError);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Something went wrong retrieving that claim. See logs for more info.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
const confirmUrl =
|
||||
this.apiServer +
|
||||
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
|
||||
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
|
||||
const confirmHeaders = await this.getHeaders(identity);
|
||||
try {
|
||||
const response = await this.axios.get(confirmUrl, {
|
||||
headers: confirmHeaders,
|
||||
});
|
||||
if (response.status === 200) {
|
||||
const resultList1 = response.data.result || [];
|
||||
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
|
||||
const resultList3 = R.reject(
|
||||
(did: string) => did === this.veriClaim.issuer,
|
||||
resultList2,
|
||||
);
|
||||
this.confirmerIdList = resultList3;
|
||||
this.numConfsNotVisible = resultList1.length - resultList2.length;
|
||||
if (resultList3.length === resultList2.length) {
|
||||
// the issuer was not in the "visible" list so they must be hidden
|
||||
// so subtract them from the non-visible confirmers count
|
||||
this.numConfsNotVisible = this.numConfsNotVisible - 1;
|
||||
}
|
||||
this.confsVisibleToIdList =
|
||||
response.data.result.resultVisibleToDids || [];
|
||||
} else {
|
||||
this.confsVisibleErrorMessage =
|
||||
"Had problems retrieving confirmations.";
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const serverError = error as AxiosError;
|
||||
console.error("Error retrieving confirmations:", serverError);
|
||||
this.confsVisibleErrorMessage =
|
||||
"Had problems retrieving confirmations. See logs for more info.";
|
||||
}
|
||||
}
|
||||
|
||||
async showFullClaim(claimId: string) {
|
||||
await accountsDB.open();
|
||||
const accounts = accountsDB.accounts;
|
||||
const accountsArr = await accounts?.toArray();
|
||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
const url =
|
||||
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
|
||||
const headers = await this.getHeaders(identity);
|
||||
|
||||
try {
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
if (resp.status === 200) {
|
||||
this.fullClaim = resp.data;
|
||||
} else {
|
||||
// actually, axios typically throws an error so we never get here
|
||||
console.log("Error getting full claim:", resp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was a problem getting that claim. See logs for more info.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error retrieving full claim:", error);
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError.response?.status === 403) {
|
||||
this.fullClaimMessage =
|
||||
"You are not authorized to view the full contents of this claim." +
|
||||
" To see all the details, ask the issuer to allow you to see their claims." +
|
||||
" If you cannot see the issuer's DID, ask someone in the Confirmations section above." +
|
||||
" If there are no connections, you will have to ask people in your" +
|
||||
" network for their help, some other way; send them to this page and" +
|
||||
" see if they can make a connection for you.";
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Something went wrong retrieving that claim. See logs for more info.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async confirmClaim() {
|
||||
if (confirm("Do you personally confirm that this is true?")) {
|
||||
// similar logic is found in endorser-mobile
|
||||
const goodClaim = serverUtil.removeSchemaContext(
|
||||
serverUtil.removeVisibleToDids(
|
||||
serverUtil.addLastClaimOrHandleAsIdIfMissing(
|
||||
this.veriClaim.claim,
|
||||
this.veriClaim.id,
|
||||
this.veriClaim.handleId,
|
||||
),
|
||||
),
|
||||
);
|
||||
const confirmationClaim: serverUtil.GenericVerifiableCredential & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
object: any;
|
||||
} = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "AgreeAction",
|
||||
object: goodClaim,
|
||||
};
|
||||
const result = await serverUtil.createAndSubmitClaim(
|
||||
confirmationClaim,
|
||||
await this.getIdentity(this.activeDid),
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
);
|
||||
if (result.type === "success") {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "Confirmation submitted.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
} else {
|
||||
console.log("Got error submitting the confirmation:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was a problem submitting the confirmation. See logs for more info.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -25,6 +25,13 @@
|
||||
<span class="justify-around">(Only 50 most recent)</span>
|
||||
<span />
|
||||
</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 -->
|
||||
<table
|
||||
@@ -124,7 +131,7 @@ interface Notification {
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class ContactsView extends Vue {
|
||||
export default class ContactAmountssView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
@@ -178,6 +185,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log("Error retrieving settings or gives.", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -185,7 +193,7 @@ export default class ContactsView extends Vue {
|
||||
title: "Error",
|
||||
text:
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -145,6 +145,7 @@ export default class ContactGiftingView extends Vue {
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log("Error retrieving settings & contacts:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -152,7 +153,7 @@ export default class ContactGiftingView extends Vue {
|
||||
title: "Error",
|
||||
text:
|
||||
err.message ||
|
||||
"There was an error retrieving the latest sweet, sweet action.",
|
||||
"There was an error retrieving your settings and/or contacts.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
|
||||
@@ -18,9 +18,19 @@
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||
Your Contact Info
|
||||
</h1>
|
||||
<p v-if="!givenName" class="text-center mt-2">
|
||||
<span class="text-red">Beware!</span>
|
||||
You aren't sharing your name, so hurry and
|
||||
<router-link
|
||||
:to="{ name: 'new-edit-account' }"
|
||||
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
||||
>
|
||||
go here to set it for them.
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div @click="onCopyToClipboard()">
|
||||
<div @click="onCopyToClipboard()" v-if="activeDid">
|
||||
<!--
|
||||
Play with display options: https://qr-code-styling.com/
|
||||
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
|
||||
@@ -32,6 +42,15 @@
|
||||
class="flex justify-center"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-center" v-else>
|
||||
You have no identitifiers yet, so
|
||||
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
||||
create your identifier.
|
||||
</router-link>
|
||||
<br />
|
||||
We recommend you do that first; otherwise, these contacts won't see your
|
||||
activity.
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
|
||||
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
||||
@@ -78,6 +97,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
givenName = "";
|
||||
qrValue = "";
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
@@ -91,7 +111,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
|
||||
if (!identity) {
|
||||
throw new Error(
|
||||
"Attempted to load Give records with no identity available.",
|
||||
"Attempted to show contact info with no identity available.",
|
||||
);
|
||||
}
|
||||
return identity;
|
||||
@@ -102,21 +122,12 @@ export default class ContactQRScanShow extends Vue {
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.givenName = settings?.firstName || "";
|
||||
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
||||
if (!account) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "",
|
||||
text: "You have no identity yet.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
if (account) {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const publicKeyHex = identity.keys[0].publicKeyHex;
|
||||
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
||||
|
||||
@@ -19,15 +19,16 @@
|
||||
</div>
|
||||
|
||||
<!-- New Contact -->
|
||||
<div class="mb-4 flex">
|
||||
<span class="self-center bg-slate-500 text-white px-1.5 py-1 rounded-md">
|
||||
<router-link :to="{ name: 'contact-qr' }">
|
||||
<fa icon="qrcode" class="fa-fw" />
|
||||
</router-link>
|
||||
</span>
|
||||
<div class="mb-4 flex items-stretch">
|
||||
<router-link
|
||||
:to="{ name: 'contact-qr' }"
|
||||
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
>
|
||||
<fa icon="qrcode" class="fa-fw text-2xl" />
|
||||
</router-link>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="DID, Name, Public Key"
|
||||
placeholder="DID, Name, Public Key (base 16 or 64)"
|
||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
||||
v-model="contactInput"
|
||||
/>
|
||||
@@ -67,8 +68,8 @@
|
||||
showGiveTotals
|
||||
? "Total"
|
||||
: showGiveConfirmed
|
||||
? "Confirmed"
|
||||
: "Unconfirmed"
|
||||
? "Confirmed"
|
||||
: "Unconfirmed"
|
||||
}}
|
||||
</button>
|
||||
<br />
|
||||
@@ -110,48 +111,50 @@
|
||||
</div>
|
||||
|
||||
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
||||
<button
|
||||
v-if="contact.seesMe"
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
@click="setVisibility(contact, false, true)"
|
||||
title="They can see you"
|
||||
>
|
||||
<fa icon="eye" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
@click="setVisibility(contact, true, true)"
|
||||
title="They cannot see you"
|
||||
>
|
||||
<fa icon="eye-slash" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
@click="checkVisibility(contact)"
|
||||
title="Check Visibility"
|
||||
>
|
||||
<fa icon="rotate" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="register(contact)"
|
||||
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
|
||||
>
|
||||
<fa
|
||||
v-if="contact.registered"
|
||||
icon="person-circle-check"
|
||||
class="fa-fw"
|
||||
title="Registered"
|
||||
/>
|
||||
<fa
|
||||
<div v-if="activeDid">
|
||||
<button
|
||||
v-if="contact.seesMe"
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
@click="setVisibility(contact, false, true)"
|
||||
title="They can see you"
|
||||
>
|
||||
<fa icon="eye" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
icon="person-circle-question"
|
||||
class="fa-fw"
|
||||
title="Registration Unknown"
|
||||
/>
|
||||
</button>
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
@click="setVisibility(contact, true, true)"
|
||||
title="They cannot see you"
|
||||
>
|
||||
<fa icon="eye-slash" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
@click="checkVisibility(contact)"
|
||||
title="Check Visibility"
|
||||
v-if="activeDid"
|
||||
>
|
||||
<fa icon="rotate" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
@click="register(contact)"
|
||||
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
|
||||
v-if="activeDid"
|
||||
>
|
||||
<fa
|
||||
v-if="contact.registered"
|
||||
icon="person-circle-check"
|
||||
class="fa-fw"
|
||||
title="Registered"
|
||||
/>
|
||||
<fa
|
||||
v-else
|
||||
icon="person-circle-question"
|
||||
class="fa-fw"
|
||||
title="Registration Unknown"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="deleteContact(contact)"
|
||||
@@ -168,7 +171,7 @@
|
||||
<button
|
||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
|
||||
@click="onClickAddGive(activeDid, contact.did)"
|
||||
title="givenByMeDescriptions[contact.did]"
|
||||
:title="givenByMeDescriptions[contact.did] || ''"
|
||||
>
|
||||
To:
|
||||
{{
|
||||
@@ -187,7 +190,7 @@
|
||||
<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"
|
||||
@click="onClickAddGive(contact.did, activeDid)"
|
||||
title="givenToMeDescriptions[contact.did]"
|
||||
:title="givenToMeDescriptions[contact.did] || ''"
|
||||
>
|
||||
From:
|
||||
{{
|
||||
@@ -263,6 +266,7 @@ import {
|
||||
SimpleSigner,
|
||||
} from "@/libs/crypto";
|
||||
import {
|
||||
CONTACT_URL_PREFIX,
|
||||
GiveServerRecord,
|
||||
GiveVerifiableCredential,
|
||||
RegisterVerifiableCredential,
|
||||
@@ -303,6 +307,7 @@ export default class ContactsView extends Vue {
|
||||
givenToMeUnconfirmed: Record<string, number> = {};
|
||||
hourDescriptionInput = "";
|
||||
hourInput = "0";
|
||||
isRegistered = false;
|
||||
showGiveNumbers = false;
|
||||
showGiveTotals = true;
|
||||
showGiveConfirmed = true;
|
||||
@@ -312,6 +317,7 @@ export default class ContactsView extends Vue {
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
this.showGiveNumbers = !!settings?.showContactGivesInline;
|
||||
if (this.showGiveNumbers) {
|
||||
@@ -330,7 +336,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
public async getIdentity(activeDid: string): Promise<IIdentifier> {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
|
||||
@@ -361,6 +367,10 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
|
||||
async loadGives() {
|
||||
if (!this.activeDid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleResponse = (
|
||||
resp: { status: number; data: { data: GiveServerRecord[] } },
|
||||
descriptions: Record<string, string>,
|
||||
@@ -395,11 +405,11 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
title: "Retrieval Error",
|
||||
text:
|
||||
"Got an error retrieving your " +
|
||||
(useRecipient ? "given" : "received") +
|
||||
" time from the server.",
|
||||
" data from the server.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -450,12 +460,13 @@ export default class ContactsView extends Vue {
|
||||
this.givenToMeConfirmed = givenToMeConfirmed;
|
||||
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
||||
} catch (error) {
|
||||
console.log("Error loading gives", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
text: error as string,
|
||||
title: "Load Error",
|
||||
text: "Got an error loading your gives.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -475,6 +486,12 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
||||
await this.newContactFromScan(this.contactInput);
|
||||
return;
|
||||
}
|
||||
|
||||
let did = this.contactInput;
|
||||
let name, publicKeyBase64;
|
||||
const commaPos1 = this.contactInput.indexOf(",");
|
||||
@@ -493,7 +510,7 @@ export default class ContactsView extends Vue {
|
||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
||||
}
|
||||
const newContact = { did, name, publicKeyBase64 };
|
||||
return this.addContact(newContact);
|
||||
await this.addContact(newContact);
|
||||
}
|
||||
|
||||
async newContactFromScan(url: string): Promise<void> {
|
||||
@@ -540,31 +557,39 @@ export default class ContactsView extends Vue {
|
||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||
allContacts,
|
||||
);
|
||||
this.setVisibility(newContact, true, false);
|
||||
let addedMessage;
|
||||
if (this.activeDid) {
|
||||
this.setVisibility(newContact, true, false);
|
||||
addedMessage =
|
||||
newContact.name +
|
||||
" was added, and your activity is visible to them.";
|
||||
} else {
|
||||
addedMessage = newContact.name + " was added.";
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Contact Added",
|
||||
text:
|
||||
newContact.name +
|
||||
" was added, and your activity is visible to them.",
|
||||
text: addedMessage,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
// 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,
|
||||
5000,
|
||||
);
|
||||
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) => {
|
||||
console.error("Error when adding contact to storage:", err);
|
||||
@@ -685,6 +710,7 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error when registering:", error);
|
||||
let userMessage = "There was an error. See logs for more info.";
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError) {
|
||||
@@ -701,7 +727,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
title: "Registration Error",
|
||||
text: userMessage,
|
||||
},
|
||||
-1,
|
||||
@@ -747,27 +773,28 @@ export default class ContactsView extends Vue {
|
||||
} else {
|
||||
console.error(
|
||||
"Got some bad server response when setting visibility: ",
|
||||
resp.status,
|
||||
resp,
|
||||
);
|
||||
const message =
|
||||
resp.data.error?.message || "Bad server response of " + resp.status;
|
||||
resp.data.error?.message || "Got some error setting visibility.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
title: "Error Setting Visibility",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Got some server error when setting visibility:", err);
|
||||
console.error("Got some error when setting visibility:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
title: "Error Setting Visibility",
|
||||
text: "Check connectivity and try again.",
|
||||
},
|
||||
-1,
|
||||
@@ -810,19 +837,19 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
title: "Error Checking Visibility",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} 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(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
title: "Error Checking Visibility",
|
||||
text: "Check connectivity and try again.",
|
||||
},
|
||||
-1,
|
||||
@@ -881,13 +908,13 @@ export default class ContactsView extends Vue {
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else if (!parseFloat(this.hourInput)) {
|
||||
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Input Error",
|
||||
text: "Giving 0 hours does nothing.",
|
||||
text: "Giving no hours or descrption does nothing.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -938,6 +965,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
// similar function is in endorserServer.ts
|
||||
private async createAndSubmitGive(
|
||||
identity: IIdentifier,
|
||||
fromDid: string,
|
||||
@@ -1007,6 +1035,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error in createAndSubmitGive: ", error);
|
||||
let userMessage = "There was an error. See logs for more info.";
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError) {
|
||||
@@ -1023,7 +1052,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
title: "Error Sending Give",
|
||||
text: userMessage,
|
||||
},
|
||||
-1,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<QuickNav selected="Discover"></QuickNav>
|
||||
<TopMessage />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
@@ -136,6 +137,7 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import TopMessage from "@/components/TopMessage.vue";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
@@ -149,6 +151,7 @@ interface Notification {
|
||||
QuickNav,
|
||||
InfiniteScroll,
|
||||
EntityIcon,
|
||||
TopMessage,
|
||||
},
|
||||
})
|
||||
export default class DiscoverView extends Vue {
|
||||
@@ -274,8 +277,8 @@ export default class DiscoverView extends Vue {
|
||||
const plans: ProjectData[] = results.data;
|
||||
if (plans) {
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, rowid });
|
||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||
}
|
||||
this.remoteCount = this.projects.length;
|
||||
} else {
|
||||
@@ -357,10 +360,14 @@ export default class DiscoverView extends Vue {
|
||||
if (beforeId) {
|
||||
const plans: ProjectData[] = results.data;
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId = plan.handleId, rowid } = plan;
|
||||
if (beforeId !== plan["rowid"]) {
|
||||
this.projects.push({ name, description, handleId, rowid });
|
||||
}
|
||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||
this.projects.push({
|
||||
name,
|
||||
description,
|
||||
handleId,
|
||||
issuerDid,
|
||||
rowid,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.projects = results.data;
|
||||
|
||||
401
src/views/HelpNotificationsView.vue
Normal file
@@ -0,0 +1,401 @@
|
||||
<template>
|
||||
<QuickNav />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="mb-8">
|
||||
<!-- Back -->
|
||||
<div class="text-lg text-center font-light relative px-7">
|
||||
<h1
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Heading -->
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||
Notification Help
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Here are ways to test notifications and get them working.</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">Full Test</h2>
|
||||
<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">If this app is not installed...</h2>
|
||||
<div>
|
||||
<p>
|
||||
For best results on mobile, install this app on your device (as
|
||||
opposed to using it inside the broser app). In Chrome, it may prompt
|
||||
you, and you can also look for the "Install" command in the browser
|
||||
settings; on the the deskop, 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">
|
||||
If "you must enable notifications"...
|
||||
</h2>
|
||||
<div>
|
||||
<p>
|
||||
Wait for about 10 seconds (for the service worker to activate), then
|
||||
<button class="text-blue-500" @click="showNotificationChoice()">
|
||||
click here.
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold">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". Also 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">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">Check 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>
|
||||
Requires Mac OS 13; see your macOS version under Apple -> About
|
||||
This Mac.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold">Windows desktop</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">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>
|
||||
<ul class="ml-4 list-disc">
|
||||
<li>
|
||||
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.)
|
||||
</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>
|
||||
|
||||
<button
|
||||
@click="showTestNotification()"
|
||||
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
Send Test Notification Directly to Device (Not Through Push Server)
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="alertWebPushSubscription()"
|
||||
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
Show Web Push Subscription Info
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
||||
<button
|
||||
@click="sendTestWebPushMessage()"
|
||||
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 and Client
|
||||
Filter)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import axios from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Buffer = require("buffer/").Buffer;
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class HelpNotificationsView extends Vue {
|
||||
$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",
|
||||
text: "You must enable notifications before testing the web push.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
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(this.subscription.getKey("auth"));
|
||||
const authB64 = auth
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
const p256dh = Buffer.from(this.subscription.getKey("p256dh"));
|
||||
const p256dhB64 = p256dh
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
const newPayload = {
|
||||
endpoint: this.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);
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Test Web Push Sent",
|
||||
text: "Check your device for the test web push message, depending on the filtering you chose.",
|
||||
},
|
||||
-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>
|
||||
@@ -30,16 +30,17 @@
|
||||
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
|
||||
<p>
|
||||
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
|
||||
leaves a permanent record -- one that came from you, and the recipient
|
||||
can prove it was for them. This is personally gratifying, but it extends
|
||||
to broader work: volunteers can get confirmation of activity and
|
||||
selectively show off their contributions and network.
|
||||
First of all, you can see what people have given, and also recognize
|
||||
gifts you've seen, in a way that leaves a permanent record -- one that
|
||||
came from you, and the recipient can prove it was for them. This is
|
||||
personally gratifying, but it extends to broader work: volunteers get
|
||||
confirmation of activity, and selectively show off their contributions
|
||||
and network.
|
||||
</p>
|
||||
<p>
|
||||
You can also record projects and plans and invite others to collaborate.
|
||||
Soon you'll be able to see when others are interested and see how much
|
||||
they're willing to contribute, even if there are conditions.
|
||||
You can show giving and also offer help to ideas, based on others'
|
||||
willingness to help out, too. You can record your own ideas and invite
|
||||
others to collaborate.
|
||||
</p>
|
||||
<p>
|
||||
This app uses the power of cryptography to build a reputation, recording
|
||||
@@ -181,6 +182,30 @@
|
||||
different page.
|
||||
</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">
|
||||
How do I access even more functionality?
|
||||
</h2>
|
||||
<p>
|
||||
There is an "Advanced" section at the bottom of the Account
|
||||
<fa icon="circle-user" /> page.
|
||||
</p>
|
||||
<p>
|
||||
There is a even more functionality in a mobile app (and more
|
||||
documentation) at
|
||||
<a href="https://endorser.ch" class="text-blue-500">
|
||||
EndorserSearch.com
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
||||
<p>
|
||||
See
|
||||
@@ -199,9 +224,7 @@
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">What app version is this?</h2>
|
||||
<p>
|
||||
{{ package.version }}
|
||||
</p>
|
||||
<p>{{ package.version }} ({{ commitHash }})</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">
|
||||
For any other questions, including removing your data:
|
||||
@@ -231,6 +254,7 @@ export default class Help extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
package = Package;
|
||||
commitHash = process.env.VUE_APP_GIT_HASH;
|
||||
|
||||
showOnboardInfo() {
|
||||
this.$notify(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<QuickNav selected="Home"></QuickNav>
|
||||
<TopMessage />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||
@@ -8,28 +10,59 @@
|
||||
|
||||
<!-- show the actions for recognizing a give -->
|
||||
<div class="mb-8">
|
||||
<div v-if="!activeDid">
|
||||
To record others' giving,
|
||||
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
||||
create your identifier.</router-link
|
||||
<div
|
||||
v-if="!isInstalled()"
|
||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||
>
|
||||
<p>
|
||||
You should install this as an app.
|
||||
<router-link
|
||||
:to="{ name: 'help-notifications' }"
|
||||
class="text-blue-500"
|
||||
>
|
||||
Go here for instructions.
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!activeDid"
|
||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||
>
|
||||
<p class="text-lg mb-3">
|
||||
You need an <b>identifier</b> before you can record anyone's gives.
|
||||
</p>
|
||||
<router-link
|
||||
:to="{ name: 'start' }"
|
||||
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
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isRegistered">
|
||||
To record others' giving, someone must register your account, so show
|
||||
them
|
||||
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
|
||||
your identity info</router-link
|
||||
<div
|
||||
v-else-if="!isRegistered"
|
||||
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 anyone's gives.
|
||||
To do this:
|
||||
<router-link
|
||||
:to="{ name: 'contact-qr' }"
|
||||
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
||||
>
|
||||
and then
|
||||
<router-link :to="{ name: 'account' }" class="text-blue-500">
|
||||
check your limits.</router-link
|
||||
1. Show Them Your Identity Info</router-link
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'account' }"
|
||||
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
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- activeDid && isRegistered -->
|
||||
<h2 class="text-xl font-bold">Record Something Given</h2>
|
||||
<h2 class="text-xl font-bold mb-4">Record Something Given</h2>
|
||||
|
||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||
<li @click="openDialog()">
|
||||
@@ -87,40 +120,53 @@
|
||||
showGivenToUser="true"
|
||||
/>
|
||||
|
||||
<!-- Results List -->
|
||||
<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>
|
||||
<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 }">
|
||||
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
||||
</p>
|
||||
</div>
|
||||
<ul class="border-t border-slate-300">
|
||||
<li
|
||||
class="border-b border-slate-300 py-2"
|
||||
v-for="record in feedData"
|
||||
:key="record.jwtId"
|
||||
>
|
||||
<div
|
||||
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
|
||||
v-if="record.jwtId == feedLastViewedId"
|
||||
>
|
||||
You've seen all claims below:
|
||||
</div>
|
||||
<div class="flex">
|
||||
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
|
||||
<!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
|
||||
<span class="">{{ this.giveDescription(record) }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import EntityIcon from "@/components/EntityIcon.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 { Account } from "@/db/tables/accounts";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import {
|
||||
@@ -128,11 +174,7 @@ import {
|
||||
GiverInputInfo,
|
||||
GiveServerRecord,
|
||||
} from "@/libs/endorserServer";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
@@ -142,7 +184,13 @@ interface Notification {
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
components: {
|
||||
GiftedDialog,
|
||||
QuickNav,
|
||||
EntityIcon,
|
||||
InfiniteScroll,
|
||||
TopMessage,
|
||||
},
|
||||
})
|
||||
export default class HomeView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
@@ -151,10 +199,9 @@ export default class HomeView extends Vue {
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
feedAllLoaded = false;
|
||||
feedData = [];
|
||||
feedPreviousOldestId?: string;
|
||||
feedLastViewedId?: string;
|
||||
feedLastViewedClaimId?: string;
|
||||
isHiddenSpinner = true;
|
||||
isRegistered = false;
|
||||
numAccounts = 0;
|
||||
@@ -171,13 +218,7 @@ export default class HomeView extends Vue {
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
throw new Error(
|
||||
"Attempted to load Give records with no identity available.",
|
||||
);
|
||||
}
|
||||
return identity;
|
||||
return identity; // may be null
|
||||
}
|
||||
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
@@ -200,11 +241,15 @@ export default class HomeView extends Vue {
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
|
||||
// this returns a Promise but we don't need to wait for it
|
||||
this.updateAllFeed();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log("Error retrieving settings and/or feed.", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -212,13 +257,25 @@ export default class HomeView extends Vue {
|
||||
title: "Error",
|
||||
text:
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// from https://benborgers.com/posts/pwa-detect-installed
|
||||
isInstalled() {
|
||||
// For iOS
|
||||
if ("standalone" in window.navigator) return true;
|
||||
|
||||
// For Android
|
||||
if (window.matchMedia("(display-mode: standalone)").matches) return true;
|
||||
|
||||
// If neither is true, it's not installed
|
||||
return false;
|
||||
}
|
||||
|
||||
public async buildHeaders() {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -245,24 +302,33 @@ export default class HomeView extends Vue {
|
||||
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() {
|
||||
this.isHiddenSpinner = false;
|
||||
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
|
||||
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
||||
.then(async (results) => {
|
||||
if (results.data.length > 0) {
|
||||
this.feedData = this.feedData.concat(results.data);
|
||||
this.feedAllLoaded = results.hitLimit;
|
||||
this.feedPreviousOldestId =
|
||||
results.data[results.data.length - 1].jwtId;
|
||||
// The following update is only done on the first load.
|
||||
if (
|
||||
this.feedLastViewedId == null ||
|
||||
this.feedLastViewedId < results.data[0].jwtId
|
||||
this.feedLastViewedClaimId == null ||
|
||||
this.feedLastViewedClaimId < results.data[0].jwtId
|
||||
) {
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
lastViewedClaimId: results.data[0].jwtId,
|
||||
});
|
||||
// but not for this page because we need to remember what it was before
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -272,17 +338,22 @@ export default class HomeView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Export Error",
|
||||
title: "Feed Error",
|
||||
text: e.userMessage || "There was an error retrieving feed data.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
});
|
||||
|
||||
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 response = await fetch(
|
||||
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
||||
@@ -347,6 +418,13 @@ export default class HomeView extends Vue {
|
||||
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
||||
}
|
||||
|
||||
onClickLoadClaim(jwtId: string) {
|
||||
const route = {
|
||||
path: "/claim/" + encodeURIComponent(jwtId),
|
||||
};
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
displayAmount(code: string, amt: number) {
|
||||
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
components: {},
|
||||
})
|
||||
export default class ImportAccountView extends Vue {
|
||||
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
|
||||
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
|
||||
|
||||
mnemonic = "";
|
||||
address = "";
|
||||
|
||||
@@ -68,7 +68,7 @@ export default class NewEditAccountView extends Vue {
|
||||
});
|
||||
localStorage.setItem("firstName", this.givenName as string);
|
||||
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
|
||||
this.$router.push({ name: "account" });
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
onClickCancel() {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
><fa icon="chevron-left" class="fa-fw"></fa
|
||||
></router-link>
|
||||
[New/Edit] Plan
|
||||
Edit Idea
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -24,22 +24,28 @@
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Project Name"
|
||||
placeholder="Idea Name"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="projectName"
|
||||
v-model="fullClaim.name"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Description"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
rows="5"
|
||||
v-model="description"
|
||||
maxlength="500"
|
||||
v-model="fullClaim.description"
|
||||
maxlength="5000"
|
||||
></textarea>
|
||||
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
||||
{{ description.length }}/500 max. characters
|
||||
{{ fullClaim.description?.length }}/5000 max. characters
|
||||
</div>
|
||||
|
||||
<input
|
||||
v-model="fullClaim.url"
|
||||
placeholder="Website"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
/>
|
||||
|
||||
<div class="flex items-center mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -72,7 +78,7 @@
|
||||
name="OpenStreetMap"
|
||||
/>
|
||||
<l-marker
|
||||
v-if="latitude || longitude"
|
||||
v-if="latitude && longitude"
|
||||
:lat-lng="[latitude, longitude]"
|
||||
@click="maybeEraseLatLong()"
|
||||
/>
|
||||
@@ -136,13 +142,17 @@ export default class NewEditProjectView extends Vue {
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
description = "";
|
||||
errorMessage = "";
|
||||
fullClaim: PlanVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "PlanAction",
|
||||
name: "",
|
||||
description: "",
|
||||
}; // this default is only to avoid errors before plan is loaded
|
||||
includeLocation = false;
|
||||
latitude = 0;
|
||||
longitude = 0;
|
||||
numAccounts = 0;
|
||||
projectName = "";
|
||||
zoom = 2;
|
||||
|
||||
async beforeCreate() {
|
||||
@@ -214,9 +224,12 @@ export default class NewEditProjectView extends Vue {
|
||||
try {
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
if (resp.status === 200) {
|
||||
const claim = resp.data.claim;
|
||||
this.projectName = claim.name;
|
||||
this.description = claim.description;
|
||||
this.fullClaim = resp.data.claim;
|
||||
if (this.fullClaim?.location) {
|
||||
this.includeLocation = true;
|
||||
this.latitude = this.fullClaim.location.geo.latitude;
|
||||
this.longitude = this.fullClaim.location.geo.longitude;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error retrieving that project", error);
|
||||
@@ -225,13 +238,7 @@ export default class NewEditProjectView extends Vue {
|
||||
|
||||
private async SaveProject(identity: IIdentifier) {
|
||||
// Make a claim
|
||||
const vcClaim: PlanVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "PlanAction",
|
||||
name: this.projectName,
|
||||
description: this.description,
|
||||
identifier: this.projectId || undefined,
|
||||
};
|
||||
const vcClaim: PlanVerifiableCredential = this.fullClaim;
|
||||
if (this.projectId) {
|
||||
vcClaim.identifier = this.projectId;
|
||||
}
|
||||
@@ -293,6 +300,20 @@ export default class NewEditProjectView extends Vue {
|
||||
2000,
|
||||
this,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"Got unexpected 'data' inside response from server",
|
||||
resp,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Saving Idea",
|
||||
text: "Server did not save the idea. Try again.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
let userMessage = "There was an error saving the project.";
|
||||
@@ -300,8 +321,8 @@ export default class NewEditProjectView extends Vue {
|
||||
error?: { message?: string };
|
||||
}>;
|
||||
if (serverError) {
|
||||
console.log("Got error from server", serverError);
|
||||
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
||||
console.log(serverError);
|
||||
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -88,7 +88,7 @@ export default class NewIdentifierView extends Vue {
|
||||
|
||||
this.loading = false;
|
||||
setTimeout(() => {
|
||||
this.$router.push({ name: "account" });
|
||||
this.$router.push({ name: "home" });
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<QuickNav selected="Projects"></QuickNav>
|
||||
<QuickNav />
|
||||
<TopMessage />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Breadcrumb -->
|
||||
@@ -12,7 +14,7 @@
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
</button>
|
||||
View Plan
|
||||
Idea
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +37,7 @@
|
||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
{{ issuer }}
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="timeSince">
|
||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||
{{ timeSince }}
|
||||
</div>
|
||||
@@ -45,8 +47,14 @@
|
||||
:href="getOpenStreetMapUrl()"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
Map View
|
||||
>Map View
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="url">
|
||||
<fa icon="globe" class="fa-fw text-slate-400"></fa>
|
||||
<a :href="addScheme(url)" target="_blank" class="underline"
|
||||
>{{ domainForWebsite(this.url) }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,8 +64,11 @@
|
||||
<div class="text-sm text-slate-500">
|
||||
<div v-if="!expanded">
|
||||
{{ truncatedDesc }}
|
||||
<a v-if="description.length >= truncateLength" @click="expandText"
|
||||
>Read More</a
|
||||
<a
|
||||
v-if="description.length >= truncateLength"
|
||||
@click="expandText"
|
||||
class="uppercase text-xs font-semibold text-slate-700"
|
||||
>... Read More</a
|
||||
>
|
||||
</div>
|
||||
<div v-else>
|
||||
@@ -65,7 +76,7 @@
|
||||
<a
|
||||
@click="collapseText"
|
||||
class="uppercase text-xs font-semibold text-slate-700"
|
||||
>Read Less</a
|
||||
>- Read Less</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,8 +91,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div v-if="activeDid" class="text-center">
|
||||
<div v-if="activeDid" class="mb-4">
|
||||
<div class="text-center">
|
||||
<button
|
||||
@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"
|
||||
@@ -91,17 +102,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div v-if="activeDid" class="text-center">
|
||||
<div v-if="activeDid">
|
||||
<div class="text-center">
|
||||
<button
|
||||
@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"
|
||||
>
|
||||
I gave…
|
||||
</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>
|
||||
<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">
|
||||
<li @click="openGiftDialog()">
|
||||
@@ -148,7 +158,7 @@
|
||||
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||
Offered To This Project
|
||||
Offered To This Idea
|
||||
</h3>
|
||||
|
||||
<div v-if="offersToThis.length === 0">
|
||||
@@ -166,9 +176,14 @@
|
||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
{{ didInfo(offer.agentDid, activeDid, allMyDids, allContacts) }}
|
||||
</span>
|
||||
<a @click="onClickLoadClaim(offer.jwtId)">
|
||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
||||
</a>
|
||||
<span v-if="offer.amount">
|
||||
<fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||
{{ offer.amount }}
|
||||
<fa
|
||||
:icon="iconForUnitCode(offer.unit)"
|
||||
class="fa-fw text-slate-400"
|
||||
/>{{ offer.amount }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="offer.objectDescription" class="text-slate-500">
|
||||
@@ -180,9 +195,7 @@
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||
Given To This Project
|
||||
</h3>
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">Given To This Idea</h3>
|
||||
|
||||
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
|
||||
|
||||
@@ -197,9 +210,14 @@
|
||||
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||
</span>
|
||||
<span v-if="give.amount"
|
||||
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||
{{ give.amount }}
|
||||
<a @click="onClickLoadClaim(give.jwtId)">
|
||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
||||
</a>
|
||||
<span v-if="give.amount">
|
||||
<fa
|
||||
:icon="iconForUnitCode(give.unit)"
|
||||
class="fa-fw text-slate-400"
|
||||
/>{{ give.amount }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="give.description" class="text-slate-500">
|
||||
@@ -216,30 +234,34 @@
|
||||
class="bg-slate-100 px-4 py-3 rounded-md"
|
||||
>
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||
Contributions To This Project
|
||||
Contributions To This Idea
|
||||
</h3>
|
||||
<ul>
|
||||
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
|
||||
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
||||
<div class="text-center">
|
||||
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
|
||||
<button
|
||||
@click="onClickLoadProject(plan.handleId)"
|
||||
class="text-blue-500"
|
||||
>
|
||||
{{ plan.name }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||
Contributions By This Project
|
||||
Contributions By This Idea
|
||||
</h3>
|
||||
<button
|
||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||
class="text-blue-500"
|
||||
>
|
||||
{{ fulfilledByThis.name }}
|
||||
</button>
|
||||
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
||||
<div class="text-center">
|
||||
<button
|
||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||
class="text-blue-500"
|
||||
>
|
||||
{{ fulfilledByThis.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,10 +285,12 @@ import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import OfferDialog from "@/components/OfferDialog.vue";
|
||||
import TopMessage from "@/components/TopMessage.vue";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { isGlobalUri } from "@/libs/util";
|
||||
import {
|
||||
didInfo,
|
||||
GiverInputInfo,
|
||||
@@ -286,7 +310,7 @@ interface Notification {
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
|
||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
|
||||
})
|
||||
export default class ProjectViewView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
@@ -309,6 +333,7 @@ export default class ProjectViewView extends Vue {
|
||||
timeSince = "";
|
||||
truncatedDesc = "";
|
||||
truncateLength = 40;
|
||||
url = "";
|
||||
|
||||
async created() {
|
||||
await db.open();
|
||||
@@ -338,12 +363,6 @@ export default class ProjectViewView extends Vue {
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
throw new Error(
|
||||
"Attempted to load project records with no identity available.",
|
||||
);
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
@@ -410,19 +429,22 @@ export default class ProjectViewView extends Vue {
|
||||
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
||||
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
||||
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
||||
} else if (resp.status === 404) {
|
||||
// actually, axios throws an error so we never get here
|
||||
this.url = resp.data.claim?.url || "";
|
||||
} else {
|
||||
// actually, axios throws an error on 404 so we probably never get here
|
||||
console.log("Error getting project:", resp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "That project does not exist.",
|
||||
text: "There was a problem getting that project. See logs for more info.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error retrieving project:", error);
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError.response?.status === 404) {
|
||||
this.$notify(
|
||||
@@ -444,7 +466,6 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error("Error retrieving project:", serverError.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,5 +648,59 @@ export default class ProjectViewView extends Vue {
|
||||
openOfferDialog() {
|
||||
(this.$refs.customOfferDialog as OfferDialog).open();
|
||||
}
|
||||
|
||||
onClickLoadClaim(jwtId: string) {
|
||||
const route = {
|
||||
path: "/claim/" + encodeURIComponent(jwtId),
|
||||
};
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
UNIT_CODES: Record<string, Record<string, string>> = {
|
||||
BTC: {
|
||||
name: "Bitcoin",
|
||||
faIcon: "bitcoin-sign",
|
||||
},
|
||||
HUR: {
|
||||
name: "hours",
|
||||
faIcon: "clock",
|
||||
},
|
||||
USD: {
|
||||
name: "US Dollars",
|
||||
faIcon: "dollar",
|
||||
},
|
||||
};
|
||||
|
||||
iconForUnitCode(unitCode: string) {
|
||||
return this.UNIT_CODES[unitCode]?.faIcon || "question";
|
||||
}
|
||||
|
||||
// return an HTTPS URL if it's not a global URL
|
||||
addScheme(url: string) {
|
||||
if (!isGlobalUri(url)) {
|
||||
return "https://" + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// return just the domain for display, if possible
|
||||
domainForWebsite(url: string) {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
if (!hostname) {
|
||||
// happens for non-http URLs
|
||||
return url;
|
||||
} else if (url.endsWith(hostname)) {
|
||||
// it's just the domain
|
||||
return hostname;
|
||||
} else {
|
||||
// there's more, but don't bother displaying the whole thing
|
||||
return hostname + "...";
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// must not be a valid URL
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<QuickNav selected="Projects"></QuickNav>
|
||||
<TopMessage />
|
||||
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Heading -->
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||
Your Plans
|
||||
Your Ideas
|
||||
</h1>
|
||||
|
||||
<!-- Quick Search -->
|
||||
@@ -79,6 +81,7 @@ import { IIdentifier } from "@veramo/core";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import TopMessage from "@/components/TopMessage.vue";
|
||||
import { ProjectData } from "@/libs/endorserServer";
|
||||
|
||||
interface Notification {
|
||||
@@ -89,7 +92,7 @@ interface Notification {
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { InfiniteScroll, QuickNav, EntityIcon },
|
||||
components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage },
|
||||
})
|
||||
export default class ProjectsView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
@@ -122,8 +125,8 @@ export default class ProjectsView extends Vue {
|
||||
if (resp.status === 200 || !resp.data.data) {
|
||||
const plans: ProjectData[] = resp.data.data;
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, rowid });
|
||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||
}
|
||||
} else {
|
||||
console.log("Bad server response & data:", resp.status, resp.data);
|
||||
|
||||
@@ -4,58 +4,121 @@ importScripts(
|
||||
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
|
||||
);
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
console.error(event);
|
||||
importScripts(
|
||||
function logConsoleAndDb(message, arg1, arg2) {
|
||||
// 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
|
||||
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 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",
|
||||
"nacl.js",
|
||||
"noble-curves.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) {
|
||||
let text = null;
|
||||
if (event.data) {
|
||||
text = event.data.text();
|
||||
}
|
||||
logConsoleAndDb("Service worker received a push event.", text, event);
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
try {
|
||||
let payload;
|
||||
if (event.data) {
|
||||
payload = JSON.parse(event.data.text());
|
||||
if (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();
|
||||
}
|
||||
if (message) {
|
||||
const options = {
|
||||
body: message,
|
||||
icon: payload ? payload.icon : "icon.png",
|
||||
badge: payload ? payload.badge : "badge.png",
|
||||
};
|
||||
await self.registration.showNotification(title, options);
|
||||
logConsoleAndDb("Notified user:", options);
|
||||
} else {
|
||||
logConsoleAndDb("No notification message.");
|
||||
}
|
||||
const message = await self.getNotificationCount();
|
||||
console.error(message);
|
||||
const title = payload ? payload.title : "Custom Title";
|
||||
const options = {
|
||||
body: message,
|
||||
icon: payload ? payload.icon : "icon.png",
|
||||
badge: payload ? payload.badge : "badge.png",
|
||||
};
|
||||
await self.registration.showNotification(title, options);
|
||||
} catch (error) {
|
||||
console.error("Error in processing the push event:", error);
|
||||
logConsoleAndDb("Error with push event", event, error);
|
||||
}
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
logConsoleAndDb("Service worker got a message...", event);
|
||||
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
|
||||
self.secret = event.data.data;
|
||||
event.ports[0].postMessage({ success: true });
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(clients.claim());
|
||||
console.log("Service worker activated", event);
|
||||
logConsoleAndDb("Service worker posted a message.");
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
console.log(event.request);
|
||||
logConsoleAndDb("Service worker got fetch event.", 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("Line:", event.lineno);
|
||||
console.error("Column:", event.colno);
|
||||
|
||||
@@ -395,12 +395,42 @@ async function setMostRecentNotified(id) {
|
||||
data["lastNotifiedClaimId"] = id;
|
||||
await updateRecord(store, data);
|
||||
} else {
|
||||
console.error("Record not found");
|
||||
console.error(
|
||||
"safari-notifications setMostRecentNotified IndexedDB settings record not found",
|
||||
);
|
||||
}
|
||||
|
||||
transaction.oncomplete = () => db.close();
|
||||
} catch (error) {
|
||||
console.error("Database error: " + error.message);
|
||||
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");
|
||||
// will 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 anything older than today
|
||||
}
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.put(data);
|
||||
@@ -430,20 +461,23 @@ function updateRecord(store, data) {
|
||||
|
||||
async function fetchAllAccounts() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let openRequest = indexedDB.open("TimeSafariAccounts");
|
||||
const openRequest = indexedDB.open("TimeSafariAccounts");
|
||||
|
||||
openRequest.onupgradeneeded = function (event) {
|
||||
let db = event.target.result;
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains("accounts")) {
|
||||
db.createObjectStore("accounts", { keyPath: "id" });
|
||||
}
|
||||
if (!db.objectStoreNames.contains("worker_log")) {
|
||||
db.createObjectStore("worker_log");
|
||||
}
|
||||
};
|
||||
|
||||
openRequest.onsuccess = function (event) {
|
||||
let db = event.target.result;
|
||||
let transaction = db.transaction("accounts", "readonly");
|
||||
let objectStore = transaction.objectStore("accounts");
|
||||
let getAllRequest = objectStore.getAll();
|
||||
const db = event.target.result;
|
||||
const transaction = db.transaction("accounts", "readonly");
|
||||
const objectStore = transaction.objectStore("accounts");
|
||||
const getAllRequest = objectStore.getAll();
|
||||
|
||||
getAllRequest.onsuccess = function () {
|
||||
resolve(getAllRequest.result);
|
||||
@@ -460,75 +494,77 @@ async function fetchAllAccounts() {
|
||||
}
|
||||
|
||||
async function getNotificationCount() {
|
||||
let secret = null;
|
||||
let accounts = [];
|
||||
let result = null;
|
||||
if ("secret" in self) {
|
||||
secret = self.secret;
|
||||
const secretUint8Array = self.decodeBase64(secret);
|
||||
const settings = await getSettingById(1);
|
||||
let lastNotifiedClaimId = null;
|
||||
if ("lastNotifiedClaimId" in settings) {
|
||||
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
|
||||
}
|
||||
const activeDid = settings["activeDid"];
|
||||
accounts = await fetchAllAccounts();
|
||||
let did = null;
|
||||
for (var i = 0; i < accounts.length; i++) {
|
||||
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(
|
||||
"https://test-api.endorser.ch/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 = "You have no new claims today.";
|
||||
} else {
|
||||
result = `${newClaims} have been shared with you`;
|
||||
}
|
||||
const most_recent_notified = claims[0]["id"];
|
||||
await setMostRecentNotified(most_recent_notified);
|
||||
} else {
|
||||
console.error(response.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// 1 is our master settings ID; see MASTER_SETTINGS_KEY
|
||||
const settings = await getSettingById(1);
|
||||
let lastNotifiedClaimId = null;
|
||||
if ("lastNotifiedClaimId" in settings) {
|
||||
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
|
||||
}
|
||||
const activeDid = settings["activeDid"];
|
||||
accounts = await fetchAllAccounts();
|
||||
let activeAccount = null;
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
if (accounts[i]["did"] == activeDid) {
|
||||
activeAccount = accounts[i];
|
||||
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;
|
||||
}
|
||||
|
||||
self.appendDailyLog = appendDailyLog;
|
||||
self.getNotificationCount = getNotificationCount;
|
||||
self.decodeBase64 = decodeBase64;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const { defineConfig } = require("@vue/cli-service");
|
||||
const { gitDescribeSync } = require("git-describe");
|
||||
|
||||
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
|
||||
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
configureWebpack: {
|
||||
|
||||
29
web-push.md
@@ -390,3 +390,32 @@ While notifications are turned on, the user can tap on the App Notifications tog
|
||||
|
||||
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
|
||||
- "Leave it On" to make no changes and dismiss the dialog.
|
||||
|
||||
# NOTIFICATION STATES
|
||||
|
||||
* Unpermissioned. Push server cannot send notifications to the user because it does not have permission.
|
||||
This may be the same as when the user gave permission in the past but has since revoked it at the OS or browser
|
||||
level, outside the app. (User can change to Permissioned when the user gives permission.)
|
||||
* Permissioned. (User can change to Unpermissioned via the OS or browser settings.)
|
||||
* Active. (User can change to Muted when the user mutes notifications.)
|
||||
* 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.)
|
||||
|
||||
|
||||
# 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
|
||||
|
||||