forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b141b6334 | ||
|
|
0f46b8379c | ||
|
|
1bd22ad260 | ||
| 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 | |||
| 1b9ae96006 | |||
|
|
4dd5664462 | ||
|
|
7d6a45061d | ||
|
|
3b32c2b156 | ||
|
|
1ee6203f4c | ||
|
|
d93299c352 | ||
|
|
9aea7a576d | ||
| 714bb169fa | |||
| ee6a344daf | |||
| 65a5edf26b |
@@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Web push notifications
|
||||
|
||||
## [0.1.3] - 2023.11
|
||||
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
|
||||
### Added
|
||||
- Contact name editing
|
||||
### Changed
|
||||
|
||||
25
README.md
25
README.md
@@ -13,6 +13,11 @@ npm install
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### 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/",`
|
||||
@@ -21,11 +26,12 @@ If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js,
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
npx prettier --write ./sw_scripts/
|
||||
```
|
||||
to make sure the service worker scripts are in proper form
|
||||
|
||||
... then copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +62,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 +94,19 @@ 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.)
|
||||
* Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers` or `about:debugging`).
|
||||
* Clear notification permission (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search).
|
||||
* Clear Cache Storage (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage).
|
||||
|
||||
|
||||
|
||||
|
||||
136
package-lock.json
generated
136
package-lock.json
generated
@@ -35,6 +35,7 @@
|
||||
"ethr-did-resolver": "^8.1.2",
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"localstorage-slim": "^2.5.0",
|
||||
"luxon": "^3.4.3",
|
||||
"merkletreejs": "^0.3.10",
|
||||
@@ -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",
|
||||
@@ -57,6 +59,7 @@
|
||||
"web-did-resolver": "^2.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/leaflet": "^1.9.4",
|
||||
"@types/ramda": "^0.29.3",
|
||||
"@types/three": "^0.155.1",
|
||||
@@ -72,13 +75,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 +2824,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 +2874,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 +5501,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 +5528,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": {
|
||||
@@ -8791,6 +8794,12 @@
|
||||
"@types/istanbul-lib-report": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
||||
@@ -8819,11 +8828,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": {
|
||||
@@ -9206,6 +9215,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",
|
||||
@@ -11054,8 +11069,7 @@
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/array-buffer-byte-length": {
|
||||
"version": "1.0.0",
|
||||
@@ -11245,7 +11259,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 +12076,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 +14232,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 +15904,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 +16163,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 +16215,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",
|
||||
@@ -16355,7 +16365,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 +16435,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 +16473,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 +16484,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 +16495,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 +17086,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 +17172,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 +17276,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 +17563,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"
|
||||
},
|
||||
@@ -18561,7 +18593,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@@ -23137,9 +23168,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 +26901,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 +27086,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 +28009,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",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"ethr-did-resolver": "^8.1.2",
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"localstorage-slim": "^2.5.0",
|
||||
"luxon": "^3.4.3",
|
||||
"merkletreejs": "^0.3.10",
|
||||
@@ -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",
|
||||
@@ -57,6 +59,7 @@
|
||||
"web-did-resolver": "^2.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/leaflet": "^1.9.4",
|
||||
"@types/ramda": "^0.29.3",
|
||||
"@types/three": "^0.155.1",
|
||||
@@ -72,13 +75,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"
|
||||
}
|
||||
|
||||
@@ -1,56 +1,36 @@
|
||||
|
||||
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
|
||||
|
||||
- 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
|
||||
- extract private_key_hex in py-push-server webpush.py
|
||||
- lock down regenerate_vapid endpoint (so only we admins can do it on demand)
|
||||
- remove sleep in py-push-server app.py
|
||||
- revisit "maybe" and "never" buttons on accont screen
|
||||
- see if we can detect OS-level notifications if turned off
|
||||
- write troubleshooting docs for notifications
|
||||
- in py-push-server, when sending a push to a subscriber and we get on a 410 "error #106", delete the subscription record
|
||||
|
||||
- .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
|
||||
|
||||
- .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)
|
||||
|
||||
- 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)
|
||||
- add note after contact addition that they can see your info
|
||||
- enhance help page instructions for debugging
|
||||
- add way to test quickly a push notification
|
||||
- help instructions for PWA install problems (secret failed, must reinstall)
|
||||
- look at other examples for better UI friend.tech
|
||||
|
||||
- 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 :
|
||||
- generate new webpush.db entry, webpush.py private_key_hex & subscription_info & vapid_claims email
|
||||
- .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions)
|
||||
- 08 thorough testing for errors & edge cases
|
||||
- 01 ensure ability to recover server remotely, and add redundant access
|
||||
@@ -60,10 +40,39 @@ 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)
|
||||
- allow some gives even if they aren't registered
|
||||
- .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 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
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
206
src/App.vue
206
src/App.vue
@@ -262,52 +262,158 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import axios from "axios";
|
||||
interface ServiceWorkerMessage {
|
||||
type: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface ServiceWorkerResponse {
|
||||
// Define the properties and their types
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Example interface for error
|
||||
interface ErrorResponse {
|
||||
message: string;
|
||||
// Other properties as needed
|
||||
}
|
||||
|
||||
interface VapidResponse {
|
||||
data: {
|
||||
vapidKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
import { AppString } 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) => {
|
||||
this.b64 = response.data.vapidKey;
|
||||
console.log(this.b64);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("API error", error);
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
let pushUrl: string = AppString.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");
|
||||
});
|
||||
});
|
||||
if (!this.b64) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Setting Notifications",
|
||||
text: "Could not set notifications.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
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(
|
||||
message: ServiceWorkerMessage,
|
||||
): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
const messageChannel = new MessageChannel();
|
||||
|
||||
messageChannel.port1.onmessage = (event: MessageEvent) => {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error as ErrorResponse);
|
||||
} else {
|
||||
resolve(event.data as ServiceWorkerResponse);
|
||||
}
|
||||
};
|
||||
|
||||
navigator.serviceWorker.controller.postMessage(message, [
|
||||
messageChannel.port2,
|
||||
]);
|
||||
} else {
|
||||
reject("Service worker controller not available");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private askPermission(): Promise<NotificationPermission> {
|
||||
// Check if Notifications are supported
|
||||
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
|
||||
return Promise.reject("Service worker not available.");
|
||||
}
|
||||
|
||||
const secret = localStorage.getItem("secret");
|
||||
if (!secret) {
|
||||
return Promise.reject("No secret found.");
|
||||
}
|
||||
|
||||
return this.sendSecretToServiceWorker(secret)
|
||||
.then(() => this.checkNotificationSupport())
|
||||
.then(() => this.requestNotificationPermission())
|
||||
.catch((error) => Promise.reject(error));
|
||||
}
|
||||
|
||||
private sendSecretToServiceWorker(secret: string): Promise<void> {
|
||||
const message: ServiceWorkerMessage = {
|
||||
type: "SEND_LOCAL_DATA",
|
||||
data: secret,
|
||||
};
|
||||
|
||||
return this.sendMessageToServiceWorker(message).then((response) => {
|
||||
console.log("Response from service worker:", response);
|
||||
});
|
||||
}
|
||||
|
||||
private checkNotificationSupport(): Promise<void> {
|
||||
if (!("Notification" in window)) {
|
||||
alert("This browser does not support notifications.");
|
||||
return Promise.reject("This browser does not support notifications.");
|
||||
}
|
||||
|
||||
// Check existing permissions
|
||||
if (Notification.permission === "granted") {
|
||||
return Promise.resolve("granted");
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Request permission
|
||||
return new Promise((resolve, reject) => {
|
||||
const permissionResult = Notification.requestPermission((result) => {
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
if (permissionResult) {
|
||||
permissionResult.then(resolve, reject);
|
||||
}
|
||||
}).then((permissionResult) => {
|
||||
console.log("Permission result:", permissionResult);
|
||||
|
||||
if (permissionResult !== "granted") {
|
||||
private requestNotificationPermission(): Promise<NotificationPermission> {
|
||||
return Notification.requestPermission().then((permission) => {
|
||||
if (permission !== "granted") {
|
||||
alert("We need notification permission to provide certain features.");
|
||||
return Promise.reject("We weren't granted permission.");
|
||||
throw new Error("We weren't granted permission.");
|
||||
}
|
||||
|
||||
return permissionResult;
|
||||
return permission;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -320,16 +426,13 @@ 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.");
|
||||
@@ -343,15 +446,17 @@ export default class App extends Vue {
|
||||
"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
|
||||
alert("Some error occurred." + error);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to convert URL base64 to Uint8Array
|
||||
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding)
|
||||
@@ -366,16 +471,25 @@ export default class App extends Vue {
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
// The subscribeToPush method
|
||||
private subscribeToPush(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if ("serviceWorker" in navigator && "PushManager" in window) {
|
||||
if (!("serviceWorker" in navigator && "PushManager" in window)) {
|
||||
const errorMsg = "Push messaging is not supported";
|
||||
console.warn(errorMsg);
|
||||
return reject(new Error(errorMsg));
|
||||
}
|
||||
|
||||
if (Notification.permission !== "granted") {
|
||||
const errorMsg = "Notification permission not granted";
|
||||
console.warn(errorMsg);
|
||||
return reject(new Error(errorMsg));
|
||||
}
|
||||
|
||||
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
|
||||
const options: PushSubscriptionOptions = {
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: applicationServerKey,
|
||||
};
|
||||
console.log(options);
|
||||
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
@@ -386,14 +500,20 @@ export default class App extends Vue {
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Push subscription failed:", error, options);
|
||||
console.error(
|
||||
"Subscription or server communication failed:",
|
||||
error,
|
||||
options,
|
||||
);
|
||||
|
||||
// Inform the user about the issue
|
||||
alert(
|
||||
"We encountered an issue setting up push notifications. " +
|
||||
"If you wish to revoke notification permissions, please do so in your browser settings.",
|
||||
);
|
||||
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
const errorMsg = "Push messaging is not supported";
|
||||
console.warn(errorMsg);
|
||||
reject(new Error(errorMsg));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -115,7 +137,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 +147,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 +173,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 +191,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 +218,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 +239,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 +263,8 @@ export default class GiftedDialog extends Vue {
|
||||
giverDid,
|
||||
this.givenToUser ? this.activeDid : undefined,
|
||||
description,
|
||||
hours,
|
||||
amountInput,
|
||||
unitCode,
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
|
||||
@@ -11,13 +11,20 @@ export enum AppString {
|
||||
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",
|
||||
|
||||
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@@ -45,5 +45,8 @@ db.on("populate", () => {
|
||||
db.settings.add({
|
||||
id: MASTER_SETTINGS_KEY,
|
||||
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
||||
|
||||
// remember that things you add from now on aren't automatically in the DB for old users
|
||||
webPushServer: AppString.DEFAULT_PUSH_SERVER,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,9 +20,9 @@ export type Settings = {
|
||||
lastViewedClaimId?: string; // Last viewed claim ID
|
||||
lastNotifiedClaimId?: string; // Last notified claim ID
|
||||
isRegistered?: boolean;
|
||||
webPushServer?: string; // Web Push server URL
|
||||
|
||||
// Array of named search boxes defined by bounding boxes
|
||||
|
||||
searchBoxes?: Array<{
|
||||
name: string;
|
||||
bbox: BoundingBox;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
5
src/libs/util.ts
Normal file
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+.-]+:/));
|
||||
};
|
||||
10
src/main.ts
10
src/main.ts
@@ -14,6 +14,7 @@ import {
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faChevronLeft,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsisVertical,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
@@ -34,16 +36,19 @@ import {
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHand,
|
||||
faHouseChimney,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faQrcode,
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
@@ -61,6 +66,7 @@ library.add(
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faChevronLeft,
|
||||
@@ -74,6 +80,7 @@ library.add(
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsisVertical,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
@@ -81,17 +88,20 @@ library.add(
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHand,
|
||||
faHouseChimney,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQrcode,
|
||||
faQuestion,
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
register("/additional-scripts.js", {
|
||||
ready() {
|
||||
console.log(
|
||||
"App is being served from cache by a service worker.\n" +
|
||||
|
||||
@@ -41,6 +41,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
|
||||
beforeEnter: enterOrStart,
|
||||
},
|
||||
{
|
||||
path: "/claim/:id?",
|
||||
name: "claim",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "claim" */ "../views/ClaimView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/confirm-contact",
|
||||
name: "confirm-contact",
|
||||
@@ -91,6 +97,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",
|
||||
|
||||
2378
src/util.d.ts
vendored
Normal file
2378
src/util.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -105,7 +105,12 @@
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input type="checkbox" name="toggleNotifications" class="sr-only" />
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="toggleNotifications"
|
||||
name="toggleNotifications"
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
@@ -136,6 +141,7 @@
|
||||
type="checkbox"
|
||||
name="toggleMuteNotifications"
|
||||
class="sr-only"
|
||||
disabled
|
||||
/>
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
@@ -202,8 +208,12 @@
|
||||
>
|
||||
Advanced
|
||||
</h3>
|
||||
|
||||
<div v-if="showAdvanced">
|
||||
<p>
|
||||
Beware: the features here can be confusing and even change data in ways
|
||||
you do not expect. But we support your freedoms!
|
||||
</p>
|
||||
|
||||
<!-- Deep Identity Details -->
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
|
||||
Deep Identity Details
|
||||
@@ -288,6 +298,14 @@
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="flex py-2">
|
||||
<button class="text-blue-500">
|
||||
<router-link :to="{ name: 'statistics' }" class="block text-center">
|
||||
See Global Animated History of Giving
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex py-2">
|
||||
<button class="text-blue-500">
|
||||
<!-- id used by puppeteer test script -->
|
||||
@@ -301,14 +319,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="flex py-4">
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
||||
<input
|
||||
@@ -324,30 +334,71 @@
|
||||
<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>
|
||||
|
||||
<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";
|
||||
@@ -381,7 +432,7 @@ interface IAccount {
|
||||
export default class AccountViewView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
Constants = AppString;
|
||||
AppConstants = AppString;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
@@ -392,6 +443,8 @@ export default class AccountViewView extends Vue {
|
||||
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 +457,16 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
showAdvanced = false;
|
||||
|
||||
private isSubscribed = false;
|
||||
|
||||
get toggleNotifications() {
|
||||
return this.isSubscribed;
|
||||
}
|
||||
|
||||
set toggleNotifications(value) {
|
||||
this.isSubscribed = value;
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
||||
try {
|
||||
// Open the accounts database
|
||||
@@ -486,6 +549,7 @@ export default class AccountViewView extends Vue {
|
||||
* @throws Will display specific messages to the user based on different errors.
|
||||
*/
|
||||
async created() {
|
||||
console.error("created");
|
||||
try {
|
||||
await db.open();
|
||||
|
||||
@@ -504,6 +568,18 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async mounted() {
|
||||
console.error("mounted()");
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
this.toggleNotifications = !!subscription;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.toggleNotifications = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes component state with values from the database or defaults.
|
||||
* @param {SettingsType} settings - Object containing settings from the database.
|
||||
@@ -516,6 +592,8 @@ export default class AccountViewView extends Vue {
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.webPushServer = (settings?.webPushServer as string) || "";
|
||||
this.webPushServerInput = (settings?.webPushServer as string) || "";
|
||||
this.showContactGives = !!settings?.showContactGivesInline;
|
||||
}
|
||||
|
||||
@@ -711,7 +789,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 +818,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 +831,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 +921,12 @@ 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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
475
src/views/ClaimView.vue
Normal file
475
src/views/ClaimView.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<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 pb-4 flex gap-4 overflow-hidden">
|
||||
<div class="overflow-hidden">
|
||||
<h2 class="text-xl">{{ 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 text-2xl">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 text-2xl mt-8">Claim</h2>
|
||||
<pre>{{ yamlVeriClaim }}</pre>
|
||||
</div>
|
||||
|
||||
<h2 class="font-bold text-2xl mt-8">Full Claim</h2>
|
||||
<p>
|
||||
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">
|
||||
<div v-if="fullClaimMessage">
|
||||
{{ fullClaimMessage }}
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
|
||||
@click="showFullClaim(veriClaim.id)"
|
||||
>
|
||||
Load Full Claim Details
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre>{{ yamlFullClaim }}</pre>
|
||||
</div>
|
||||
|
||||
<a :href="apiServer + '/api/claim/' + veriClaim.id" target="_blank">
|
||||
<button class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4">
|
||||
View on the Public Server
|
||||
</button>
|
||||
</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";
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
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.json;
|
||||
this.yamlVeriClaim = yaml.dumps(resp.json);
|
||||
} 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.json;
|
||||
this.yamlFullClaim = yaml.dump(resp.json);
|
||||
} 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>
|
||||
@@ -20,7 +20,7 @@
|
||||
</h1>
|
||||
</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 +32,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" />
|
||||
@@ -91,7 +100,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;
|
||||
@@ -106,17 +115,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
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");
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</span>
|
||||
<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"
|
||||
/>
|
||||
@@ -110,6 +110,7 @@
|
||||
</div>
|
||||
|
||||
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
||||
<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"
|
||||
@@ -126,18 +127,18 @@
|
||||
>
|
||||
<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"
|
||||
@@ -152,6 +153,7 @@
|
||||
title="Registration Unknown"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="deleteContact(contact)"
|
||||
@@ -263,6 +265,7 @@ import {
|
||||
SimpleSigner,
|
||||
} from "@/libs/crypto";
|
||||
import {
|
||||
CONTACT_URL_PREFIX,
|
||||
GiveServerRecord,
|
||||
GiveVerifiableCredential,
|
||||
RegisterVerifiableCredential,
|
||||
@@ -303,6 +306,7 @@ export default class ContactsView extends Vue {
|
||||
givenToMeUnconfirmed: Record<string, number> = {};
|
||||
hourDescriptionInput = "";
|
||||
hourInput = "0";
|
||||
isRegistered = false;
|
||||
showGiveNumbers = false;
|
||||
showGiveTotals = true;
|
||||
showGiveConfirmed = true;
|
||||
@@ -312,6 +316,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 +335,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;
|
||||
@@ -475,6 +480,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 +504,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,18 +551,25 @@ export default class ContactsView extends Vue {
|
||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||
allContacts,
|
||||
);
|
||||
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,
|
||||
5000,
|
||||
);
|
||||
if (this.isRegistered) {
|
||||
// putting this last so that it shows on the top
|
||||
this.$notify(
|
||||
{
|
||||
@@ -565,6 +583,7 @@ export default class ContactsView extends Vue {
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error when adding contact to storage:", err);
|
||||
@@ -938,6 +957,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
// similar function is in endorserServer.ts
|
||||
private async createAndSubmitGive(
|
||||
identity: IIdentifier,
|
||||
fromDid: string,
|
||||
|
||||
@@ -358,10 +358,8 @@ export default class DiscoverView extends Vue {
|
||||
const plans: ProjectData[] = results.data;
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId = plan.handleId, rowid } = plan;
|
||||
if (beforeId !== plan["rowid"]) {
|
||||
this.projects.push({ name, description, handleId, rowid });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.projects = results.data;
|
||||
}
|
||||
|
||||
65
src/views/HelpNotificationsView.vue
Normal file
65
src/views/HelpNotificationsView.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<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 things to try to get notifications working.</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">Test</h2>
|
||||
<p>Somehow call the service-worker self.showNotification</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">Check OS-level permissions</h2>
|
||||
<p>
|
||||
Walk-throughs & screenshots, maybe for all combinations of OS &
|
||||
browsers.
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">Check browser-level permissions</h2>
|
||||
<p>Walk-throughs & screenshots for browser settings</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">Explain full reset to start again</h2>
|
||||
<p>
|
||||
Walk-throughs for clearing everything & subscribing anew to get a
|
||||
message
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">Auto-detection</h2>
|
||||
<p>Show results of auto-detection whether they're turned on</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
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;
|
||||
}
|
||||
</script>
|
||||
@@ -181,6 +181,21 @@
|
||||
different page.
|
||||
</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
|
||||
|
||||
@@ -104,12 +104,14 @@
|
||||
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:
|
||||
You've seen all the following
|
||||
</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>
|
||||
<a @click="onClickLoadClaim(record.jwtId)">
|
||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -171,13 +173,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) {
|
||||
@@ -347,6 +343,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 = "";
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<QuickNav selected="Projects"></QuickNav>
|
||||
<QuickNav />
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Breadcrumb -->
|
||||
@@ -12,7 +12,7 @@
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
</button>
|
||||
View Plan
|
||||
Idea
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,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 +45,13 @@
|
||||
:href="getOpenStreetMapUrl()"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
Map View
|
||||
>Map View
|
||||
</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 +61,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 +73,7 @@
|
||||
<a
|
||||
@click="collapseText"
|
||||
class="uppercase text-xs font-semibold text-slate-700"
|
||||
>Read Less</a
|
||||
>- Read Less</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,7 +156,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 +174,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 +193,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 +208,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,7 +232,7 @@
|
||||
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">
|
||||
@@ -232,7 +248,7 @@
|
||||
|
||||
<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)"
|
||||
@@ -267,6 +283,7 @@ 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,
|
||||
@@ -309,6 +326,7 @@ export default class ProjectViewView extends Vue {
|
||||
timeSince = "";
|
||||
truncatedDesc = "";
|
||||
truncateLength = 40;
|
||||
url = "";
|
||||
|
||||
async created() {
|
||||
await db.open();
|
||||
@@ -410,19 +428,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 +465,6 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error("Error retrieving project:", serverError.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,5 +647,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,33 +1,69 @@
|
||||
const notifications = require("./safari-notifications.js");
|
||||
/* eslint-env serviceworker */
|
||||
/* global workbox */
|
||||
importScripts(
|
||||
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
|
||||
);
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
console.error("Adding event listener for:", event);
|
||||
importScripts(
|
||||
"safari-notifications.js",
|
||||
"nacl.js",
|
||||
"noble-curves.js",
|
||||
"noble-hashes.js",
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("push", function (event) {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
try {
|
||||
let payload;
|
||||
if (event.data) {
|
||||
payload = JSON.parse(event.data.text());
|
||||
}
|
||||
|
||||
const message = await self.getNotificationCount();
|
||||
if (message) {
|
||||
console.log("Will notify user:", message);
|
||||
const title = payload ? payload.title : "Custom Title";
|
||||
const options = {
|
||||
body: payload ? payload.body : "Custom body text",
|
||||
body: message,
|
||||
icon: payload ? payload.icon : "icon.png",
|
||||
badge: payload ? payload.badge : "badge.png",
|
||||
};
|
||||
|
||||
event.waitUntil(self.registration.showNotification(title, options));
|
||||
await self.registration.showNotification(title, options);
|
||||
} else {
|
||||
console.log("No notification message, so will not tell the user.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing the push event:", error);
|
||||
}
|
||||
})(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
self.addEventListener("message", function (event) {
|
||||
const data = event.data;
|
||||
|
||||
const result = notifications.getNotificationCount()
|
||||
|
||||
switch (data.command) {
|
||||
case "account":
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("Unknown command:", data.command);
|
||||
self.addEventListener("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);
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
console.log("Got fetch event", event.request);
|
||||
});
|
||||
|
||||
self.addEventListener("error", (event) => {
|
||||
console.error("Error in Service Worker:", event.message);
|
||||
console.error("File:", event.filename);
|
||||
console.error("Line:", event.lineno);
|
||||
console.error("Column:", event.colno);
|
||||
console.error("Error Object:", event.error);
|
||||
});
|
||||
|
||||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
1051
sw_scripts/nacl.js
Normal file
1051
sw_scripts/nacl.js
Normal file
File diff suppressed because it is too large
Load Diff
5248
sw_scripts/noble-curves.js
Normal file
5248
sw_scripts/noble-curves.js
Normal file
File diff suppressed because it is too large
Load Diff
3068
sw_scripts/noble-hashes.js
Normal file
3068
sw_scripts/noble-hashes.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,9 @@ module.exports = defineConfig({
|
||||
iconPaths: {
|
||||
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
||||
},
|
||||
workboxPluginMode: "InjectManifest",
|
||||
workboxOptions: {
|
||||
importScripts: ["additional-scripts.js"],
|
||||
swSrc: "./sw_scripts/additional-scripts.js",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
10
web-push.md
10
web-push.md
@@ -390,3 +390,13 @@ 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.)
|
||||
|
||||
Reference in New Issue
Block a user