Compare commits

..

1 Commits

Author SHA1 Message Date
7f0d10d93d Merge branch 'master' into sw-cleanup 2023-12-08 23:08:03 -05:00
36 changed files with 405 additions and 3722 deletions

View File

@@ -8,30 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.1.6] - 2023.12.17
### Added
- Infinite scroll on home page
### Changed
- UI improvements
- Show web-push subscription info
- Icon
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
### Added
- Web push notifications (though not finalized)
- Credentials details page
- See more data without an ID
- Change units of a give
## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db
### Added
- Offer on a project
### Changed
- Automatically set as visible when importing a contact
- Web push notifications
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
### Added

View File

@@ -22,25 +22,16 @@ npm run lint
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
* `npx prettier --write ./sw_scripts/`
```
npm run build
```
...to make sure the service worker scripts are in proper form
```
npx prettier --write ./sw_scripts/
```
to make sure the service worker scripts are in proper form
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit.
* Tag wth the new version: `git tag 0.1.0`. Increment version, add "-beta", `npm install`, and commit.
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be PROD.
* `npm run build`
* Revert src/constants/app.ts
* `cp sw_scripts/[ns]* dist/`
... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari`
... then copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
@@ -71,7 +62,7 @@ Under the "Your Identity" screen, click "Advanced", click "Switch Identity / No
### Web-push
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.
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
### Icons
@@ -112,10 +103,9 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
### Clear/Reset data & restart
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data".)
* Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`).
* Clear notification permission (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications").
* Clear Cache Storage (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage).
* Clear cache for localhost.
* 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).

163
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "TimeSafari",
"version": "0.1.7-beta",
"name": "crowd-funder-for-time-pwa",
"version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "TimeSafari",
"version": "0.1.7-beta",
"name": "crowd-funder-for-time-pwa",
"version": "0.1.4",
"dependencies": {
"@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
@@ -33,7 +33,6 @@
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2",
"git-describe": "^4.1.1",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"localstorage-slim": "^2.5.0",
@@ -50,7 +49,6 @@
"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",
@@ -74,13 +72,13 @@
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.15",
"eslint": "^8.53.0",
"eslint": "^8.48.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.1.0",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.3",
"typescript": "~5.2.2"
}
@@ -2823,9 +2821,9 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
"integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
@@ -2873,9 +2871,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
"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==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5500,12 +5498,12 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.1",
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.5"
},
@@ -5527,9 +5525,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"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==",
"dev": true
},
"node_modules/@jest/create-cache-key-function": {
@@ -8821,11 +8819,11 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.10.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
"version": "20.8.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
"integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
"dependencies": {
"undici-types": "~5.26.4"
"undici-types": "~5.25.1"
}
},
"node_modules/@types/normalize-package-data": {
@@ -8895,7 +8893,8 @@
"node_modules/@types/semver": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz",
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw=="
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==",
"dev": true
},
"node_modules/@types/send": {
"version": "0.17.2",
@@ -9207,12 +9206,6 @@
"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",
@@ -11252,6 +11245,7 @@
"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"
},
@@ -12069,6 +12063,7 @@
"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"
@@ -14225,19 +14220,18 @@
}
},
"node_modules/eslint": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
"version": "8.51.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.55.0",
"@humanwhocodes/config-array": "^0.11.13",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "8.51.0",
"@humanwhocodes/config-array": "^0.11.11",
"@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",
@@ -15897,6 +15891,7 @@
"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"
}
@@ -16156,6 +16151,7 @@
"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"
}
@@ -16208,6 +16204,7 @@
"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",
@@ -16272,30 +16269,6 @@
"node": ">=6"
}
},
"node_modules/git-describe": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/git-describe/-/git-describe-4.1.1.tgz",
"integrity": "sha512-JC8ganO5kO80G8+XE98TDDjnMXQN3Estk3qdJuG2EGRF/l6zuMTMcN+8OSfQZ5FWpqIRLB015anWX4aSRgnxAQ==",
"dependencies": {
"@types/semver": "^7.3.8",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=4.0.0"
},
"optionalDependencies": {
"semver": "^5.6.0"
}
},
"node_modules/git-describe/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"optional": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -16382,6 +16355,7 @@
"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"
},
@@ -16452,6 +16426,7 @@
"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"
}
@@ -16490,6 +16465,7 @@
"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"
},
@@ -16501,6 +16477,7 @@
"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"
},
@@ -16512,6 +16489,7 @@
"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"
},
@@ -17103,21 +17081,6 @@
"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",
@@ -17189,6 +17152,7 @@
"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"
},
@@ -17293,20 +17257,6 @@
"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",
@@ -17580,6 +17530,7 @@
"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"
},
@@ -19362,7 +19313,8 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"devOptional": true
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
@@ -23185,9 +23137,9 @@
}
},
"node_modules/prettier": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
@@ -26918,9 +26870,9 @@
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
"version": "5.25.3",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
@@ -27103,18 +27055,6 @@
"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",
@@ -28026,6 +27966,7 @@
"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",

View File

@@ -1,6 +1,6 @@
{
"name": "TimeSafari",
"version": "0.1.7-beta",
"name": "crowd-funder-for-time-pwa",
"version": "0.1.4",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@@ -33,7 +33,6 @@
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2",
"git-describe": "^4.1.1",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"localstorage-slim": "^2.5.0",
@@ -50,7 +49,6 @@
"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",

View File

@@ -1,39 +1,29 @@
tasks:
- 08 notifications :
- get it to work on Android - background data in apps
- get it to work on iOS
- make the app behave correctly when App Notifications are turned off
- 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
- hide the "App Notifications" toggle when they switch notifications
- prompt user to install on their home screen https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
- warn if they're using the web (android only?)
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
https://web.dev/articles/get-installed-related-apps
- add windows & mac help at OS & browser level
- back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity
- fix the projects on /discover to show the issuer (currently all "Someone Anonymous")
- .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
- .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui
- Got error adding on Firefox user #0 as contact for themselves
- .5 Add infinite scroll to gifts on the home page
- .5 If notifications are not enabled, add message to front page with link/button to enable
- 01 server - show all claim details when issued by the issuer
- enhance help page instructions for debugging
- add way to test quickly a push notification
- help instructions for PWA install problems (secret failed, must reinstall)
- look at other examples for better UI friend.tech
- show VC details... somehow:
- 01 show my VCs - most interesting, or via search
- .5 make a VC details page, or link to endorser.ch (including confirmations)
- 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
@@ -43,13 +33,10 @@ 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
- 01 send visibility signal as a VC and store it
- remove 'rowid' references (that are sqlite-specific)
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- 01 make the prod build copy the sw_scripts
- 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?
@@ -63,9 +50,6 @@ tasks:
- 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)
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
- contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 show error to user when adding a duplicate contact
@@ -107,8 +91,6 @@ tasks:
- automated tests, eg. cypress
- Notifications (wake on the phone, push notifications)
- pull instead of push, maybe via scheduled runs
- have a notification pop-up on Mac screen
- Connect with phone contacts
@@ -121,11 +103,12 @@ tasks:
- Do we want split first name & last name?
- 40 notifications v+ :
- pull, w/ scheduled runs
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
- 16 From the home screen, make the quick action even easier.
- allow some gives even if they aren't registered - maybe someday as a gift to the world, but we really want this to be built via personal connections
log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -169,12 +169,20 @@
>
Turn on Notifications
</button>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Maybe Later
</button>
<div class="grid grid-cols-2 gap-2">
<button
@click="maybeLater(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Maybe Later
</button>
<button
@click="never(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
>
Never
</button>
</div>
</div>
</div>
</div>
@@ -230,10 +238,6 @@
</p>
<button
@click="
close(notification.id);
turnOffNotifications();
"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
>
Turn Off Notifications
@@ -328,23 +332,16 @@ export default class App extends Vue {
);
}
} catch (error) {
if (window.location.host.startsWith("localhost")) {
console.log(
"Ignoring this error getting VAPID for local development:",
error,
);
} else {
console.error("Got an error initializing notifications:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Got an error setting notifications.",
},
-1,
);
}
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,
);
}
}
@@ -413,10 +410,7 @@ export default class App extends Vue {
private requestNotificationPermission(): Promise<NotificationPermission> {
return Notification.requestPermission().then((permission) => {
if (permission !== "granted") {
alert(
"Allow this app permission to make notifications for personal reminders." +
" You can adjust them at any time in your settings.",
);
alert("We need notification permission to provide certain features.");
throw new Error("We weren't granted permission.");
}
return permission;
@@ -452,22 +446,16 @@ export default class App extends Vue {
"Subscription or server communication failed:",
error,
);
alert(
"Subscription or server communication failed. Try again in a while.",
);
alert( "Subscription or server communication failed. Try again in a while." );
});
})
.catch((error) => {
console.error(
"An error occurred setting notification permissions:",
error,
);
alert(
"Some error occurred setting notification permissions. See logs.",
);
console.error("An error occurred:", error);
alert( "Some error occurred." + error );
});
}
private urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
@@ -531,7 +519,7 @@ export default class App extends Vue {
private sendSubscriptionToServer(
subscription: PushSubscription,
): Promise<void> {
console.log("About to send subscription", subscription);
console.log(subscription);
return fetch("/web-push/subscribe", {
method: "POST",
headers: {
@@ -546,49 +534,12 @@ export default class App extends Vue {
});
}
async turnOffNotifications() {
let subscription;
const pushProviderSuccess = await navigator.serviceWorker.ready
.then((registration) => {
return registration.pushManager.getSubscription();
})
.then((subscript) => {
subscription = subscript;
if (subscription) {
return subscription.unsubscribe();
} else {
console.log("Subscription object is not available.");
return false;
}
})
.catch((error) => {
console.log("Push provider server communication failed:", error);
return false;
});
never(ID: string) {
alert(ID);
}
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subscription),
})
.then((response) => {
return response.ok;
})
.catch((error) => {
console.log("Push server communication failed:", error);
return false;
});
alert(
"Notifications are off. Push provider unsubscribe " +
(pushProviderSuccess ? "succeeded" : "failed") +
(pushProviderSuccess === pushServerSuccess ? " and" : " but") +
" push server unsubscribe " +
(pushServerSuccess ? "succeeded" : "failed") +
".",
);
maybeLater(ID: string) {
alert(ID);
}
}
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -13,21 +13,18 @@
<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"
@click="changeUnitCode()"
>Hours</span
>
{{ 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="amountInput"
v-model="hours"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@@ -84,31 +81,12 @@ export default class GiftedDialog extends Vue {
activeDid = "";
apiServer = "";
amountInput = "0";
giver?: GiverInputInfo; // undefined means no identified giver agent
description = "";
givenToUser = false;
unitCode = "HUR";
hours = "0";
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();
@@ -137,7 +115,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.amountInput = "0";
this.hours = "0";
this.visible = true;
}
@@ -147,21 +125,12 @@ 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.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
}
decrement() {
this.amountInput = `${Math.max(
0,
(parseFloat(this.amountInput) || 1) - 1,
)}`;
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
}
cancel() {
@@ -173,7 +142,7 @@ export default class GiftedDialog extends Vue {
this.description = "";
this.giver = undefined;
this.givenToUser = this.showGivenToUser;
this.amountInput = "0";
this.hours = "0";
}
async confirm() {
@@ -191,8 +160,7 @@ export default class GiftedDialog extends Vue {
await this.recordGive(
this.giver?.did as string | undefined,
this.description,
parseFloat(this.amountInput),
this.unitCode,
parseFloat(this.hours),
).then(() => {
this.eraseValues();
});
@@ -218,13 +186,12 @@ export default class GiftedDialog extends Vue {
*
* @param giverDid may be null
* @param description may be an empty string
* @param amountInput may be 0
* @param hours may be 0
*/
public async recordGive(
giverDid?: string,
description?: string,
amountInput?: number,
unitCode?: string,
hours?: number,
) {
if (!this.activeDid) {
this.$notify(
@@ -239,15 +206,13 @@ export default class GiftedDialog extends Vue {
return;
}
if (!description && !amountInput) {
if (!description && !hours) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${
this.UNIT_LONG[this.unitCode]
}.`,
text: "You must enter a description or some number of hours.",
},
-1,
);
@@ -263,8 +228,7 @@ export default class GiftedDialog extends Vue {
giverDid,
this.givenToUser ? this.activeDid : undefined,
description,
amountInput,
unitCode,
hours,
this.projectId,
);

View File

@@ -1,58 +0,0 @@
<template>
<div class="text-center text-red-500">{{ message }}</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { AppString } from "@/constants/app";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component
export default class TopMessage extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@Prop selected = "";
message = "";
async mounted() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
if (
settings?.warnIfTestServer &&
window.location.hostname !== AppString.PROD_TIME_SAFARI_SERVER_HOST
) {
const didPrefix = settings.activeDid?.slice(11, 14);
this.message = "You're linked to a test server, user " + didPrefix;
} else if (
settings?.warnIfProdServer &&
window.location.hostname === AppString.PROD_TIME_SAFARI_SERVER_HOST
) {
const didPrefix = settings.activeDid?.slice(1, 14);
this.message =
"You're linked to the production server, user " + didPrefix;
}
} catch (err: unknown) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Detecting Server",
text: JSON.stringify(err),
},
10000,
);
}
}
}
</script>

View File

@@ -6,16 +6,12 @@
export enum AppString {
APP_NAME = "Time Safari",
PROD_TIME_SAFARI_SERVER_HOST = "timesafari.app",
PROD_TIME_SAFARI_SERVER = "https://" + PROD_TIME_SAFARI_SERVER_HOST,
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",

View File

@@ -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,
});
});

View File

@@ -12,17 +12,15 @@ export type BoundingBox = {
* Settings type encompasses user-specific configuration details.
*/
export type Settings = {
id: number; // Only one entry, keyed with MASTER_SETTINGS_KEY
id: number; // Only one entry using MASTER_SETTINGS_KEY
activeDid?: string; // Active Decentralized ID
apiServer?: string; // API server URL
firstName?: string; // User's first name
isRegistered?: boolean;
lastName?: string; // deprecated - put all names in firstName
lastNotifiedClaimId?: string; // Last notified claim ID
lastName?: string; // User's last name
lastViewedClaimId?: string; // Last viewed claim ID
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders
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<{
@@ -32,9 +30,8 @@ export type Settings = {
showContactGivesInline?: boolean; // Display contact inline or not
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server
webPushServer?: string; // Web Push server URL
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
reminderOn?: boolean; // Toggle to enable or disable reminders
};
/**

View File

@@ -12,8 +12,6 @@ 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;
@@ -40,24 +38,14 @@ export interface ClaimResult {
error: { code: string; message: string };
}
export interface GenericVerifiableCredential {
export interface GenericClaim {
"@context": string;
"@type": string;
}
export interface GenericServerRecord extends GenericVerifiableCredential {
handleId?: string;
id?: string;
issuedAt?: string;
issuer?: string;
issuedAt: string;
// "any" because arbitrary objects can be subject of agreement
// 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;
@@ -155,104 +143,6 @@ 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
@@ -310,7 +200,6 @@ export async function createAndSubmitGive(
toDid?: string,
description?: string,
hours?: number,
unitCode?: string,
fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitClaimResult> {
const vcClaim: GiveVerifiableCredential = {
@@ -319,15 +208,13 @@ export async function createAndSubmitGive(
recipient: toDid ? { identifier: toDid } : undefined,
agent: fromDid ? { identifier: fromDid } : undefined,
description: description || undefined,
object: hours
? { amountOfThisGood: hours, unitCode: unitCode || "HUR" }
: undefined,
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
fulfills: fulfillsProjectHandleId
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
: undefined,
};
return createAndSubmitClaim(
vcClaim as GenericServerRecord,
vcClaim as GenericClaim,
identity,
apiServer,
axios,
@@ -375,7 +262,7 @@ export async function createAndSubmitOffer(
};
}
return createAndSubmitClaim(
vcClaim as GenericServerRecord,
vcClaim as GenericClaim,
identity,
apiServer,
axios,
@@ -383,7 +270,7 @@ export async function createAndSubmitOffer(
}
export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential,
vcClaim: GenericClaim,
identity: IIdentifier,
apiServer: string,
axios: Axios,

View File

@@ -1,5 +1,3 @@
// 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+.-]+:/));
};

View File

@@ -43,7 +43,6 @@ import {
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
@@ -95,7 +94,6 @@ library.add(
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,

View File

@@ -39,12 +39,7 @@ const routes: Array<RouteRecordRaw> = [
name: "account",
component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
},
{
path: "/claim/:id?",
name: "claim",
component: () =>
import(/* webpackChunkName: "claim" */ "../views/ClaimView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/confirm-contact",

2184
src/util.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
@@ -34,24 +32,8 @@
</span>
</div>
<!-- ID notice -->
<div
v-if="!activeDid"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Note:</b> Before you can take any action, you need an ID.
</p>
<router-link
:to="{ name: 'start' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
>
Generate Identity
</router-link>
</div>
<!-- Registration notice -->
<!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
<div
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
@@ -76,9 +58,9 @@
<span v-else>
<router-link
:to="{ name: 'new-edit-account' }"
class="block w-full text-center text-md text-slate-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
>
(Set Your Name)
(set name)
</router-link>
</span>
@@ -106,25 +88,16 @@
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<label
v-if="notificationUnchanged"
for="toggleNotifications"
class="flex items-center justify-between cursor-pointer"
class="flex items-center cursor-pointer"
@click="
!toggleNotifications
? this.$notify(
{
group: 'modal',
type: 'notification-permission',
},
-1,
)
: this.$notify(
{
group: 'modal',
type: 'notification-off',
},
-1,
)
this.$notify(
{
group: 'modal',
type: 'notification-permission',
},
-1,
)
"
>
<!-- label -->
@@ -146,9 +119,37 @@
></div>
</div>
</label>
<label v-else>
Notification status may have changed. Revisit this page to see the
latest setting.
<label
for="toggleMuteNotifications"
class="flex items-center cursor-pointer mt-4"
@click="
this.$notify(
{
group: 'modal',
type: 'notification-mute',
},
-1,
)
"
>
<!-- label -->
<div>Mute Notifications</div>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
name="toggleMuteNotifications"
class="sr-only"
disabled
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
</div>
@@ -156,7 +157,7 @@
<router-link
:to="{ name: 'seed-backup' }"
v-if="activeDid"
href=""
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Backup Identifier Seed
@@ -171,42 +172,30 @@
</a>
<a ref="downloadLink" />
<div v-if="activeDid" class="my-8">
<h3 class="text-sm uppercase font-semibold mb-3">Rate Limits</h3>
<button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
@click="checkLimits()"
>
<div v-if="activeDid" class="flex py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits
</button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center mb-4">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="mb-4">
<div class="ml-2">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime">
<p class="mb-3 text-sm">
You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of
<b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims
counter resets at
<b class="whitespace-nowrap">{{
readableTime(limits.nextWeekBeginDateTime)
}}</b>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p class="text-sm">
You have done
<b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of
<b>{{ limits.maxRegistrationsPerMonth }}</b> for this month.
<i
>(You can register nobody on your first day, and after that only one
a day in your first month.)</i
>
Your registration counter resets at
<b class="whitespace-nowrap">
{{ readableTime(limits.nextMonthBeginDateTime) }}
</b>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. (You can
register nobody on your first day, and after that only one a day in
your first month.) Your registration counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
@@ -219,14 +208,10 @@
>
Advanced
</h3>
<div v-if="showAdvanced">
<p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedom!
</p>
<div v-if="showAdvanced">
<!-- Deep Identity Details -->
<h2 class="text-sm uppercase font-semibold mb-3">
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
Deep Identity Details
</h2>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
@@ -284,11 +269,13 @@
<label
for="toggleShowAmounts"
class="flex items-center justify-between cursor-pointer my-4"
class="flex items-center cursor-pointer py-2"
@click="handleChange"
>
<!-- label -->
<h2>Show amounts given with contacts</h2>
<h2 class="text-slate-500 text-sm font-bold mb-2">
Show amounts given with contacts
</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
@@ -307,28 +294,26 @@
</div>
</label>
<router-link
:to="{ name: 'statistics' }"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
See Global Animated History of Giving
</router-link>
<div class="flex py-2">
<button class="text-blue-500">
<router-link :to="{ name: 'statistics' }" class="block text-center">
See Global Animated History of Giving
</router-link>
</button>
</div>
<!-- id used by puppeteer test script -->
<router-link
id="switch-identity-link"
:to="{ name: 'identity-switcher' }"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Switch Identity
</router-link>
<button
@click="alertWebPushSubscription()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Show Subscription from Web Push Server
</button>
<div class="flex py-2">
<button class="text-blue-500">
<!-- id used by puppeteer test script -->
<router-link
id="switch-identity-link"
:to="{ name: 'identity-switcher' }"
class="block text-center"
>
Switch Identity / No Identity
</router-link>
</button>
</div>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
@@ -364,46 +349,6 @@
</button>
</div>
<label
for="toggleProdWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleProdWarning"
>
<!-- label -->
<h2>Show warning if on prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<label
for="toggleTestWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleTestWarning"
>
<!-- label -->
<h2>Show warning if on test server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server
@@ -455,7 +400,6 @@ import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@@ -480,7 +424,7 @@ interface IAccount {
derivationPath: string;
}
@Component({ components: { QuickNav, TopMessage } })
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@@ -492,7 +436,6 @@ export default class AccountViewView extends Vue {
derivationPath = "";
givenName = "";
isRegistered = false;
notificationUnchanged = true;
numAccounts = 0;
publicHex = "";
publicBase64 = "";
@@ -510,73 +453,14 @@ export default class AccountViewView extends Vue {
showAdvanced = false;
subscription: PushSubscription | null = null;
warnIfProdServer = false;
warnIfTestServer = false;
private isSubscribed = false;
get toggleNotifications() {
return this.isSubscribed;
}
set toggleNotifications(value) {
this.isSubscribed = value;
this.notificationUnchanged = false;
}
/**
* Async function executed when the component is created.
* Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
}
async mounted() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
this.toggleNotifications = !!this.subscription;
} catch (error) {
console.error("Mount error:", error);
this.toggleNotifications = false;
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline;
this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
}
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
@@ -644,16 +528,6 @@ export default class AccountViewView extends Vue {
this.updateShowContactAmounts();
}
toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer;
this.updateWarnIfProdServer(this.warnIfProdServer);
}
toggleTestWarning() {
this.warnIfTestServer = !this.warnIfTestServer;
this.updateWarnIfTestServer(this.warnIfTestServer);
}
readableTime(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T"));
}
@@ -663,6 +537,62 @@ export default class AccountViewView extends Vue {
this.numAccounts = await accountsDB.accounts.count();
}
/**
* Async function executed when the component is created.
* Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
console.error("created");
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
}
async mounted() {
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.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
this.showContactGives = !!settings?.showContactGivesInline;
}
/**
* Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information.
@@ -736,52 +666,6 @@ export default class AccountViewView extends Vue {
}
}
public async updateWarnIfProdServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Prod Warning",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error(
"Telling user to clear cache after contact setting update because:",
err,
);
}
}
public async updateWarnIfTestServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Test Warning",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error(
"Telling user to clear cache after contact setting update because:",
err,
);
}
}
/**
* Asynchronously exports the database into a downloadable JSON file.
*
@@ -1039,23 +923,6 @@ export default class AccountViewView extends Vue {
webPushServer: this.webPushServerInput,
});
this.webPushServer = this.webPushServerInput;
this.$notify(
{
group: "alert",
type: "warning",
title: "Reload",
text: "Now reload the app to get a new VAPID to use with this push server.",
},
-1,
);
}
alertWebPushSubscription() {
console.log(
"Web push subscription:",
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
);
alert(JSON.stringify(this.subscription));
}
}
</script>

View File

@@ -1,476 +0,0 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
Verifiable Claim Details
</h1>
</div>
<!-- Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div>
<div class="block flex gap-4 overflow-hidden">
<div class="overflow-hidden">
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
<div class="text-sm">
<div>
{{ veriClaim.claimType }}
</div>
<div>
<fa icon="message" class="fa-fw text-slate-400"></fa>
{{ veriClaim.claim?.description }}
</div>
<div>
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{ veriClaim.issuer }}
</div>
<div>
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
<span v-else-if="totalConfirmers() === 1">
One person has confirmed this.
</span>
<span v-else> {{ totalConfirmers() }} people have confirmed this. </span>
<div v-if="totalConfirmers() > 0">
<div
v-if="
confirmerIdList.length === 0 && confsVisibleToIdList.length === 0
"
>
Nobody that you know confirmed this claim, nor do they have any
confirmers in their network.
</div>
<div
v-if="confirmerIdList.length === 0 && confsVisibleToIdList.length > 0"
>
<!-- Only show if this person has links to confirmers (below). -->
Nobody that you know has issued or confirmed this claim.
</div>
<div v-if="confirmerIdList.length > 0">
The following people have issued or confirmed this claim.
<ul>
<li
v-for="confirmerId in confirmerIdList"
:key="confirmerId"
class="list-disc"
>
<div class="flex gap-4">
<div class="grow overflow-hidden">
<div class="text-sm">
{{ confirmerId }}
</div>
</div>
</div>
</li>
</ul>
</div>
<!--
Never need to show the following message.
If there is nobody in the confirmerIdList then we'll show the combined "nobody" message.
If there is somebody in the confirmerIdList then that's all they need to show.
-->
<!-- Nobody that you know can see someone who has confirmed this claim. -->
<div v-if="confsVisibleToIdList.length > 0">
The following people can connect you with people who have issued or
confirmed this claim.
<ul>
<li
v-for="confsVisibleTo in confsVisibleToIdList"
:key="confsVisibleTo"
class="list-disc"
>
<div class="flex gap-4">
<div class="grow overflow-hidden">
<div class="text-sm">
{{ confsVisibleTo }}
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
<div class="mt-4">
<div v-if="confirmerIdList.includes(activeDid)">
You have confirmed this claim.
</div>
<div v-else-if="containsHiddenDid(veriClaim.claim)">
You cannot confirm this claim because it contains data that is hidden
from you.
</div>
<div v-else>
<button
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
@click="confirmClaim(veriClaim.id)"
>
Confirm Claim
</button>
</div>
</div>
</div>
<div>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2>
<pre class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md">
{{ util.inspect(veriClaim, false, null) }}
</pre>
</div>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2>
<p class="mb-4">
The full claim includes the claim as it was originally issued, including
the signature (ie. the proof of issuance by that person).
</p>
<div v-if="!fullClaim">
<p v-if="fullClaimMessage" class="mb-4">
{{ fullClaimMessage }}
</p>
<button
v-else
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
@click="showFullClaim(veriClaim.id)"
>
Load Full Claim Details
</button>
</div>
<div v-else>
<pre>{{ util.inspect(fullClaim, false, null) }}</pre>
</div>
<a
:href="apiServer + '/api/claim/' + veriClaim.id"
target="_blank"
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
>
View on the Public Server
</a>
</section>
</template>
<script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import * as util from "util";
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
})
export default class ClaimView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";
allMyDids: Array<string> = [];
allContacts: Array<Contact> = [];
apiServer = "";
confirmerIdList = []; // list of DIDs that have confirmed this claim excluding the issuer
confsVisibleErrorMessage = "";
confsVisibleToIdList = []; // list of DIDs that can see any confirmer
fullClaim = null;
fullClaimMessage = "";
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
util = util;
containsHiddenDid = serverUtil.containsHiddenDid;
async created() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const accounts = accountsDB.accounts;
const accountsArr = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
const pathParam = window.location.pathname.substring("/claim/".length);
let claimId;
if (pathParam) {
claimId = decodeURIComponent(pathParam);
this.loadClaim(claimId, identity);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "No claim ID was provided.",
},
-1,
);
}
}
totalConfirmers() {
return (
this.numConfsNotVisible +
this.confirmerIdList.length +
this.confsVisibleToIdList.length
);
}
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
// Isn't there a better way to make this available to the template?
didInfo(
did: string,
activeDid: string,
dids: Array<string>,
contacts: Array<Contact>,
) {
return serverUtil.didInfo(did, activeDid, dids, contacts);
}
async loadClaim(claimId: string, identity: IIdentifier) {
const url =
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(claimId);
const headers = await this.getHeaders(identity);
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.veriClaim = resp.data;
} else {
// actually, axios typically throws an error so we never get here
console.log("Error getting claim:", resp);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem getting that claim. See logs for more info.",
},
-1,
);
}
} catch (error: unknown) {
const serverError = error as AxiosError;
console.error("Error retrieving claim:", serverError);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving that claim. See logs for more info.",
},
-1,
);
}
const confirmUrl =
this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await this.getHeaders(identity);
try {
const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders,
});
if (response.status === 200) {
const resultList1 = response.data.result || [];
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
const resultList3 = R.reject(
(did: string) => did === this.veriClaim.issuer,
resultList2,
);
this.confirmerIdList = resultList3;
this.numConfsNotVisible = resultList1.length - resultList2.length;
if (resultList3.length === resultList2.length) {
// the issuer was not in the "visible" list so they must be hidden
// so subtract them from the non-visible confirmers count
this.numConfsNotVisible = this.numConfsNotVisible - 1;
}
this.confsVisibleToIdList =
response.data.result.resultVisibleToDids || [];
} else {
this.confsVisibleErrorMessage =
"Had problems retrieving confirmations.";
}
} catch (error: unknown) {
const serverError = error as AxiosError;
console.error("Error retrieving confirmations:", serverError);
this.confsVisibleErrorMessage =
"Had problems retrieving confirmations. See logs for more info.";
}
}
async showFullClaim(claimId: string) {
await accountsDB.open();
const accounts = accountsDB.accounts;
const accountsArr = await accounts?.toArray();
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
const url =
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
const headers = await this.getHeaders(identity);
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.fullClaim = resp.data;
} else {
// actually, axios typically throws an error so we never get here
console.log("Error getting full claim:", resp);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem getting that claim. See logs for more info.",
},
-1,
);
}
} catch (error: unknown) {
console.error("Error retrieving full claim:", error);
const serverError = error as AxiosError;
if (serverError.response?.status === 403) {
this.fullClaimMessage =
"You are not authorized to view the full contents of this claim." +
" To see all the details, ask the issuer to allow you to see their claims." +
" If you cannot see the issuer's DID, ask someone in the Confirmations section above." +
" If there are no connections, you will have to ask people in your" +
" network for their help, some other way; send them to this page and" +
" see if they can make a connection for you.";
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving that claim. See logs for more info.",
},
-1,
);
}
}
}
async confirmClaim() {
if (confirm("Do you personally confirm that this is true?")) {
// similar logic is found in endorser-mobile
const goodClaim = serverUtil.removeSchemaContext(
serverUtil.removeVisibleToDids(
serverUtil.addLastClaimOrHandleAsIdIfMissing(
this.veriClaim.claim,
this.veriClaim.id,
this.veriClaim.handleId,
),
),
);
const confirmationClaim: serverUtil.GenericVerifiableCredential & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any;
} = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
await this.getIdentity(this.activeDid),
this.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted.",
},
5000,
);
} else {
console.log("Got error submitting the confirmation:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation. See logs for more info.",
},
-1,
);
}
}
}
}
</script>

View File

@@ -124,7 +124,7 @@ interface Notification {
}
@Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue {
export default class ContactsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = "";

View File

@@ -18,16 +18,6 @@
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info
</h1>
<p v-if="!givenName" class="text-center mt-2">
<span class="text-red">Beware!</span>
You aren't sharing your name, so hurry and
<router-link
:to="{ name: 'new-edit-account' }"
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
>
go here to set it for them.
</router-link>
</p>
</div>
<div @click="onCopyToClipboard()" v-if="activeDid">
@@ -97,7 +87,6 @@ export default class ContactQRScanShow extends Vue {
activeDid = "";
apiServer = "";
givenName = "";
qrValue = "";
public async getIdentity(activeDid: string) {
@@ -122,7 +111,6 @@ export default class ContactQRScanShow extends Vue {
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.givenName = settings?.firstName || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();

View File

@@ -19,13 +19,12 @@
</div>
<!-- New Contact -->
<div class="mb-4 flex items-stretch">
<router-link
:to="{ name: 'contact-qr' }"
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw text-2xl" />
</router-link>
<div class="mb-4 flex">
<span class="self-center bg-slate-500 text-white px-1.5 py-1 rounded-md">
<router-link :to="{ name: 'contact-qr' }">
<fa icon="qrcode" class="fa-fw" />
</router-link>
</span>
<input
type="text"
placeholder="DID, Name, Public Key (base 16 or 64)"
@@ -171,7 +170,7 @@
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
@click="onClickAddGive(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''"
title="givenByMeDescriptions[contact.did]"
>
To:
{{
@@ -190,7 +189,7 @@
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
@click="onClickAddGive(contact.did, activeDid)"
:title="givenToMeDescriptions[contact.did] || ''"
title="givenToMeDescriptions[contact.did]"
>
From:
{{
@@ -336,7 +335,7 @@ export default class ContactsView extends Vue {
}
}
public async getIdentity(activeDid: string): Promise<IIdentifier> {
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
@@ -367,10 +366,6 @@ export default class ContactsView extends Vue {
}
async loadGives() {
if (!this.activeDid) {
return;
}
const handleResponse = (
resp: { status: number; data: { data: GiveServerRecord[] } },
descriptions: Record<string, string>,
@@ -405,11 +400,11 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Retrieval Error",
title: "Server Error",
text:
"Got an error retrieving your " +
(useRecipient ? "given" : "received") +
" data from the server.",
" time from the server.",
},
-1,
);
@@ -460,13 +455,12 @@ export default class ContactsView extends Vue {
this.givenToMeConfirmed = givenToMeConfirmed;
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
} catch (error) {
console.log("Error loading gives", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Load Error",
text: "Got an error loading your gives.",
title: "Server Error",
text: error as string,
},
-1,
);
@@ -710,7 +704,6 @@ export default class ContactsView extends Vue {
);
}
} catch (error) {
console.error("Error when registering:", error);
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
@@ -727,7 +720,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Registration Error",
title: "Server Error",
text: userMessage,
},
-1,
@@ -773,28 +766,27 @@ export default class ContactsView extends Vue {
} else {
console.error(
"Got some bad server response when setting visibility: ",
resp.status,
resp,
);
const message =
resp.data.error?.message || "Got some error setting visibility.";
resp.data.error?.message || "Bad server response of " + resp.status;
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Visibility",
title: "Server Error",
text: message,
},
-1,
);
}
} catch (err) {
console.error("Got some error when setting visibility:", err);
console.error("Got some server error when setting visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Visibility",
title: "Server Error",
text: "Check connectivity and try again.",
},
-1,
@@ -837,19 +829,19 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Error Checking Visibility",
title: "Server Error",
text: message,
},
-1,
);
}
} catch (err) {
console.log("Caught error from request to check visibility:", err);
console.log("Caught error from server request to check visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Checking Visibility",
title: "Server Error",
text: "Check connectivity and try again.",
},
-1,
@@ -1035,7 +1027,6 @@ export default class ContactsView extends Vue {
}
}
} catch (error) {
console.log("Error in createAndSubmitGive: ", error);
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
@@ -1052,7 +1043,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Error Sending Give",
title: "Server Error",
text: userMessage,
},
-1,

View File

@@ -1,6 +1,5 @@
<template>
<QuickNav selected="Discover"></QuickNav>
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
@@ -137,7 +136,6 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
interface Notification {
group: string;
@@ -151,7 +149,6 @@ interface Notification {
QuickNav,
InfiniteScroll,
EntityIcon,
TopMessage,
},
})
export default class DiscoverView extends Vue {

View File

@@ -28,35 +28,13 @@
<p>Somehow call the service-worker self.showNotification</p>
<h2 class="text-xl font-semibold">Check OS-level permissions</h2>
<div>
<p>
Walk-throughs & screenshots, maybe for all combinations of OS &
browsers.
<div>
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
<div>
Notifications require iOS 16.4 or higher. To check your iOS version,
go to Settings > General > About > Software Version.
</div>
<h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3>
<div>See the browser section.</div>
<h3 class="text-lg font-semibold">Desktop - Mac</h3>
<div>Requires Mac OS 13.</div>
</div>
</div>
</p>
<h2 class="text-xl font-semibold">Check browser-level permissions</h2>
<p>Walk-throughs & screenshots for browser settings</p>
<div>
<div>
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
<div>Make sure your OS (above) supports it.</div>
<h3 class="text-lg font-semibold">Mobile Phone - Android</h3>
<div>Chrome requires version 50.</div>
</div>
</div>
<h2 class="text-xl font-semibold">Explain full reset to start again</h2>
<p>

View File

@@ -214,7 +214,9 @@
</p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<p>{{ package.version }} ({{ commitHash }})</p>
<p>
{{ package.version }}
</p>
<h2 class="text-xl font-semibold">
For any other questions, including removing your data:
@@ -244,7 +246,6 @@ export default class Help extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
package = Package;
commitHash = process.env.VUE_APP_GIT_HASH;
showOnboardInfo() {
this.$notify(

View File

@@ -1,7 +1,5 @@
<template>
<QuickNav selected="Home"></QuickNav>
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
@@ -10,44 +8,28 @@
<!-- show the actions for recognizing a give -->
<div class="mb-8">
<div
v-if="!activeDid"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<p class="text-lg mb-3">
You need an <b>identifier</b> before you can record anyone's gives.
</p>
<router-link
:to="{ name: 'start' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
Create Your Identifier</router-link
<div v-if="!activeDid">
To record others' giving,
<router-link :to="{ name: 'start' }" class="text-blue-500">
create your identifier.</router-link
>
</div>
<div
v-else-if="!isRegistered"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
Someone must register your account before you can record anyone's gives.
To do this:
<router-link
:to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
<div v-else-if="!isRegistered">
To record others' giving, someone must register your account, so show
them
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
your identity info</router-link
>
1. Show Them Your Identity Info</router-link
>
<router-link
:to="{ name: 'account' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
2. Check Your Limits</router-link
and then
<router-link :to="{ name: 'account' }" class="text-blue-500">
check your limits.</router-link
>
</div>
<div v-else>
<!-- activeDid && isRegistered -->
<h2 class="text-xl font-bold mb-4">Record Something Given</h2>
<h2 class="text-xl font-bold">Record Something Given</h2>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()">
@@ -105,53 +87,40 @@
showGivenToUser="true"
/>
<!-- Results List -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
<InfiniteScroll @reached-bottom="loadMoreGives">
<ul class="border-t border-slate-300">
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedClaimId"
>
You've seen all the following
</div>
<div class="flex">
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
<span class="">{{ this.giveDescription(record) }}</span>
<a @click="onClickLoadClaim(record.jwtId)">
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
</a>
</div>
</li>
</ul>
</InfiniteScroll>
<div :class="{ hidden: isHiddenSpinner }">
<p class="text-slate-500 text-center italic mt-4 mb-4">
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</p>
</div>
<ul class="border-t border-slate-300">
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId"
>
You've seen all 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>
</div>
</li>
</ul>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
@@ -159,7 +128,11 @@ import {
GiverInputInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
@@ -169,13 +142,7 @@ interface Notification {
}
@Component({
components: {
GiftedDialog,
QuickNav,
EntityIcon,
InfiniteScroll,
TopMessage,
},
components: { GiftedDialog, QuickNav, EntityIcon },
})
export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@@ -184,9 +151,10 @@ export default class HomeView extends Vue {
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
feedAllLoaded = false;
feedData = [];
feedPreviousOldestId?: string;
feedLastViewedClaimId?: string;
feedLastViewedId?: string;
isHiddenSpinner = true;
isRegistered = false;
numAccounts = 0;
@@ -226,12 +194,9 @@ export default class HomeView extends Vue {
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.feedLastViewedId = settings?.lastViewedClaimId;
this.isRegistered = !!settings?.isRegistered;
// this returns a Promise but we don't need to wait for it
this.updateAllFeed();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
this.$notify(
@@ -274,33 +239,24 @@ export default class HomeView extends Vue {
return headers;
}
/**
* Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load
**/
public async loadMoreGives(payload: boolean) {
if (payload) {
this.updateAllFeed();
}
}
public async updateAllFeed() {
this.isHiddenSpinner = false;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data);
this.feedAllLoaded = results.hitLimit;
this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId;
// The following update is only done on the first load.
if (
this.feedLastViewedClaimId == null ||
this.feedLastViewedClaimId < results.data[0].jwtId
this.feedLastViewedId == null ||
this.feedLastViewedId < results.data[0].jwtId
) {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId,
});
// but not for this page because we need to remember what it was before
}
}
})
@@ -310,22 +266,17 @@ export default class HomeView extends Vue {
{
group: "alert",
type: "danger",
title: "Feed Error",
title: "Export Error",
text: e.userMessage || "There was an error retrieving feed data.",
},
-1,
);
});
this.isHiddenSpinner = true;
}
/**
* Retrieve claims in reverse chronological order
*
* @param beforeId the earliest ID (of previous searches) to search earlier
* @return claims in reverse chronological order
*/
public async retrieveGives(endorserApiServer: string, beforeId?: string) {
public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch(
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
@@ -390,13 +341,6 @@ 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);
}

View File

@@ -68,7 +68,7 @@ export default class NewEditAccountView extends Vue {
});
localStorage.setItem("firstName", this.givenName as string);
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
this.$router.back();
this.$router.push({ name: "account" });
}
onClickCancel() {

View File

@@ -37,7 +37,7 @@
maxlength="5000"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ fullClaim.description?.length }}/5000 max. characters
{{ fullClaim.description.length }}/5000 max. characters
</div>
<input

View File

@@ -88,7 +88,7 @@ export default class NewIdentifierView extends Vue {
this.loading = false;
setTimeout(() => {
this.$router.push({ name: "home" });
this.$router.push({ name: "account" });
}, 1000);
}
}

View File

@@ -1,7 +1,5 @@
<template>
<QuickNav />
<TopMessage />
<QuickNav selected="Projects"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
@@ -90,8 +88,8 @@
</button>
</div>
<div v-if="activeDid" class="mb-4">
<div class="text-center">
<div class="mb-4">
<div v-if="activeDid" class="text-center">
<button
@click="openOfferDialog({ name: 'you', did: activeDid })"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
@@ -101,16 +99,17 @@
</div>
</div>
<div v-if="activeDid">
<div class="text-center">
<div>
<div v-if="activeDid" class="text-center">
<button
@click="openGiftDialog({ name: 'you', did: activeDid })"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
>
I gave&hellip;
</button>
<p class="mt-2 mb-4 text-center">Or, record a contribution from:</p>
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
</div>
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openGiftDialog()">
@@ -175,9 +174,6 @@
<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="iconForUnitCode(offer.unit)"
@@ -209,9 +205,6 @@
><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
</span>
<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)"
@@ -235,32 +228,28 @@
<h3 class="text-sm uppercase font-semibold mb-3">
Contributions To This Idea
</h3>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center">
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
<ul>
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
<button
@click="onClickLoadProject(plan.handleId)"
class="text-blue-500"
>
{{ plan.name }}
</button>
</div>
</div>
</li>
</ul>
</div>
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">
Contributions By This Idea
</h3>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center">
<button
@click="onClickLoadProject(fulfilledByThis.handleId)"
class="text-blue-500"
>
{{ fulfilledByThis.name }}
</button>
</div>
<button
@click="onClickLoadProject(fulfilledByThis.handleId)"
class="text-blue-500"
>
{{ fulfilledByThis.name }}
</button>
</div>
</div>
</div>
@@ -284,7 +273,6 @@ import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue";
import TopMessage from "@/components/TopMessage.vue";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@@ -309,7 +297,7 @@ interface Notification {
}
@Component({
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
})
export default class ProjectViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@@ -362,6 +350,12 @@ export default class ProjectViewView extends Vue {
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
@@ -429,21 +423,19 @@ export default class ProjectViewView extends Vue {
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
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);
} else if (resp.status === 404) {
// actually, axios throws an error so we never get here
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem getting that project. See logs for more info.",
text: "That project does not exist.",
},
-1,
);
}
} catch (error: unknown) {
console.error("Error retrieving project:", error);
const serverError = error as AxiosError;
if (serverError.response?.status === 404) {
this.$notify(
@@ -465,6 +457,7 @@ export default class ProjectViewView extends Vue {
},
-1,
);
console.error("Error retrieving project:", serverError.message);
}
}
@@ -648,13 +641,6 @@ export default class ProjectViewView extends Vue {
(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",

View File

@@ -1,11 +1,9 @@
<template>
<QuickNav selected="Projects"></QuickNav>
<TopMessage />
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Ideas
Your Plans
</h1>
<!-- Quick Search -->
@@ -81,7 +79,6 @@ import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
import { ProjectData } from "@/libs/endorserServer";
interface Notification {
@@ -92,7 +89,7 @@ interface Notification {
}
@Component({
components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage },
components: { InfiniteScroll, QuickNav, EntityIcon },
})
export default class ProjectsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;

View File

@@ -5,7 +5,7 @@ importScripts(
);
self.addEventListener("install", (event) => {
console.log("Adding event listener for:", event);
console.error("Adding event listener for:", event);
importScripts(
"safari-notifications.js",
"nacl.js",

View File

@@ -395,12 +395,12 @@ async function setMostRecentNotified(id) {
data["lastNotifiedClaimId"] = id;
await updateRecord(store, data);
} else {
console.error("IndexedDB settings record not found.");
console.error("Record not found");
}
transaction.oncomplete = () => db.close();
} catch (error) {
console.error("IndexedDB error:", error);
console.error("Database error: " + error.message);
}
}
@@ -520,11 +520,7 @@ async function getNotificationCount() {
const most_recent_notified = claims[0]["id"];
await setMostRecentNotified(most_recent_notified);
} else {
console.error(
"The service worker got a bad response status when fetching claims:",
response.status,
response,
);
console.error("The service worker got a bad response status when fetching claims:", response.status, response);
}
break;
}

View File

@@ -1,8 +1,4 @@
const { defineConfig } = require("@vue/cli-service");
const { gitDescribeSync } = require("git-describe");
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {

View File

@@ -400,22 +400,3 @@ While notifications are turned on, the user can tap on the App Notifications tog
* Active. (User can change to Muted when the user mutes notifications.)
* Muted. (User can change to Active when the user toggles it.)
(Turning mute off automatically after some amount of time is not planned in version 1.)
# TROUBLESHOOTING
## Desktop
#### Firefox
Go to `about:debugging` and click on `Inspect` for the service worker.
#### Chrome
Go to `chrome://inspect/#service-workers` and click on `Inspect` for the service worker.
## Mobile
#### Android
#### iOS