Compare commits

..

1 Commits

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

View File

@@ -8,48 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.1.8] - 2023.12.27
### Added ### Added
- DB logging for service-worker events - Web push notifications
- Help page for notifications
- Test notification & web-push triggers inside app
- Check that the app is installed
### Fixed
- Project issuer display name
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
### Changed
- Icons
### Fixed
- Notification switch now shows message
- Prod/test server warning message at top of page
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
### Added
- Infinite scroll on home page
### Changed
- UI improvements
- Show web-push subscription info
- Icon
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
### Added
- Web push notifications (though not finalized)
- Credentials details page
- See more data without an ID
- Change units of a give
## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db
### Added
- Offer on a project
### Changed
- Automatically set as visible when importing a contact
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde ## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
### Added ### 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/",` If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit. ```
npm run build
```
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) ```
npx prettier --write ./sw_scripts/
```
to make sure the service worker scripts are in proper form
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test". ... then copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
* `npm run build`
* `npx prettier --write ./sw_scripts/`
...to make sure the service worker scripts are in proper form. It's only important if you changed something in that directory.
* `cp sw_scripts/[ns]* dist/`
... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari`
* Revert src/constants/app.ts and/or package.json, edit package.json to increment version & add "-beta", `npm install`, and commit.
@@ -71,7 +62,7 @@ Under the "Your Identity" screen, click "Advanced", click "Switch Identity / No
### Web-push ### 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 ### Icons
@@ -112,12 +103,9 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
### Clear/Reset data & restart ### Clear/Reset data & restart
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.) * Clear cache for localhost.
* Clear notification permission. (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".) * Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers` or `about:debugging`).
* 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).
* Clear Cache Storage. (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
(If you find more, add them to the HelpNotificationsView.vue file.)
@@ -140,4 +128,3 @@ Gifts make the world go 'round!
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org * [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) * [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg) * [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)

163
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "TimeSafari_Test", "name": "crowd-funder-for-time-pwa",
"version": "0.1.8", "version": "0.1.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TimeSafari_Test", "name": "crowd-funder-for-time-pwa",
"version": "0.1.8", "version": "0.1.4",
"dependencies": { "dependencies": {
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/fontawesome-svg-core": "^6.4.2",
@@ -33,7 +33,6 @@
"ethereum-cryptography": "^2.1.2", "ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2", "ethr-did-resolver": "^8.1.2",
"git-describe": "^4.1.1",
"jdenticon": "^3.2.0", "jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"localstorage-slim": "^2.5.0", "localstorage-slim": "^2.5.0",
@@ -50,7 +49,6 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"three": "^0.156.1", "three": "^0.156.1",
"util": "^0.12.5",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.2", "vue-facing-decorator": "^3.0.2",
@@ -74,13 +72,13 @@
"@vue/cli-service": "~5.0.8", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"eslint": "^8.53.0", "eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"postcss": "^8.4.29", "postcss": "^8.4.29",
"prettier": "^3.1.0", "prettier": "^3.0.3",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "~5.2.2" "typescript": "~5.2.2"
} }
@@ -2823,9 +2821,9 @@
} }
}, },
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "2.1.4", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ajv": "^6.12.4", "ajv": "^6.12.4",
@@ -2873,9 +2871,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "8.55.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz",
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -5500,12 +5498,12 @@
} }
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.13", "version": "0.11.11",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@humanwhocodes/object-schema": "^2.0.1", "@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
"minimatch": "^3.0.5" "minimatch": "^3.0.5"
}, },
@@ -5527,9 +5525,9 @@
} }
}, },
"node_modules/@humanwhocodes/object-schema": { "node_modules/@humanwhocodes/object-schema": {
"version": "2.0.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true "dev": true
}, },
"node_modules/@jest/create-cache-key-function": { "node_modules/@jest/create-cache-key-function": {
@@ -8821,11 +8819,11 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.10.4", "version": "20.8.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", "integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.25.1"
} }
}, },
"node_modules/@types/normalize-package-data": { "node_modules/@types/normalize-package-data": {
@@ -8895,7 +8893,8 @@
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.3", "version": "7.5.3",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", "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": { "node_modules/@types/send": {
"version": "0.17.2", "version": "0.17.2",
@@ -9207,12 +9206,6 @@
"url": "https://opencollective.com/typescript-eslint" "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": { "node_modules/@unimodules/core": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/@unimodules/core/-/core-7.1.2.tgz", "resolved": "https://registry.npmjs.org/@unimodules/core/-/core-7.1.2.tgz",
@@ -11252,6 +11245,7 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -12069,6 +12063,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"devOptional": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2" "get-intrinsic": "^1.0.2"
@@ -14225,19 +14220,18 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.55.0", "version": "8.51.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4", "@eslint/eslintrc": "^2.1.2",
"@eslint/js": "8.55.0", "@eslint/js": "8.51.0",
"@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/config-array": "^0.11.11",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@@ -15897,6 +15891,7 @@
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
"dev": true,
"dependencies": { "dependencies": {
"is-callable": "^1.1.3" "is-callable": "^1.1.3"
} }
@@ -16156,6 +16151,7 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"devOptional": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@@ -16208,6 +16204,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"devOptional": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
@@ -16272,30 +16269,6 @@
"node": ">=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": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -16382,6 +16355,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"dev": true,
"dependencies": { "dependencies": {
"get-intrinsic": "^1.1.3" "get-intrinsic": "^1.1.3"
}, },
@@ -16452,6 +16426,7 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
"integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
"devOptional": true,
"engines": { "engines": {
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
@@ -16490,6 +16465,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"devOptional": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -16501,6 +16477,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"devOptional": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -16512,6 +16489,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"dev": true,
"dependencies": { "dependencies": {
"has-symbols": "^1.0.2" "has-symbols": "^1.0.2"
}, },
@@ -17103,21 +17081,6 @@
"node": ">= 0.10" "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": { "node_modules/is-array-buffer": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
@@ -17189,6 +17152,7 @@
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -17293,20 +17257,6 @@
"node": ">=4" "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": { "node_modules/is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -17580,6 +17530,7 @@
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
"dev": true,
"dependencies": { "dependencies": {
"which-typed-array": "^1.1.11" "which-typed-array": "^1.1.11"
}, },
@@ -19362,7 +19313,8 @@
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "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": { "node_modules/lodash.clonedeep": {
"version": "4.5.0", "version": "4.5.0",
@@ -23185,9 +23137,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.1.0", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"dev": true, "dev": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
@@ -26918,9 +26870,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "5.26.5", "version": "5.25.3",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
}, },
"node_modules/unicode-canonical-property-names-ecmascript": { "node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0", "version": "2.0.0",
@@ -27103,18 +27055,6 @@
"resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz",
"integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -28026,6 +27966,7 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
"integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
"dev": true,
"dependencies": { "dependencies": {
"available-typed-arrays": "^1.0.5", "available-typed-arrays": "^1.0.5",
"call-bind": "^1.0.2", "call-bind": "^1.0.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "TimeSafari_Test", "name": "crowd-funder-for-time-pwa",
"version": "0.1.8", "version": "0.1.4",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@@ -33,7 +33,6 @@
"ethereum-cryptography": "^2.1.2", "ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2", "ethr-did-resolver": "^8.1.2",
"git-describe": "^4.1.1",
"jdenticon": "^3.2.0", "jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"localstorage-slim": "^2.5.0", "localstorage-slim": "^2.5.0",
@@ -50,7 +49,6 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"three": "^0.156.1", "three": "^0.156.1",
"util": "^0.12.5",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.2", "vue-facing-decorator": "^3.0.2",

View File

@@ -1,27 +1,29 @@
tasks: tasks:
- 08 notifications : - 40 notifications :
- .2 after turning on notification, don't wait in push server but wait in client for message test - push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- insert tooling (exportable logs?) so that we can see problems and troubleshoot as we onboard - extract private_key_hex in py-push-server webpush.py
- if navigator.serviceWorker is null, then tell the user to wait - lock down regenerate_vapid endpoint (so only we admins can do it on demand)
- Local install works after cleared out cache in Chrome - 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
- fix maskable icon - .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
- give users next public key hash - .5 Add infinite scroll to gifts on the home page
- .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
- .5 If notifications are not enabled, add message to front page with link/button to enable - .5 If notifications are not enabled, add message to front page with link/button to enable
- show VC details... somehow: - 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) - 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 - 04 allow user to download VCs, mine + ones I can see about me from others
- add VC confirmation? - add VC confirmation?
- Release Minimum Viable Product : - 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) - .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 - 08 thorough testing for errors & edge cases
- 01 ensure ability to recover server remotely, and add redundant access - 01 ensure ability to recover server remotely, and add redundant access
@@ -31,20 +33,10 @@ tasks:
- Deploy to a server. - Deploy to a server.
- Ensure public server has limits that work for group adoption. - Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS. - 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 blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too) - make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- 02 watch for the service worker activation before showing the button to turn on notifications - allow some gives even if they aren't registered
- 01 server - show all claim details when issued by the issuer
- bug - got error adding on Firefox user #0 as contact for themselves
- bug (that is hard to reproduce) - back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity
- bug (that is hard to reproduce) - in Chrome, install app then delete app and try from Chrome browser and see log errors "Uncaught TypeError: self.appendDailyLog is not a function"
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
- 01 send visibility signal as a VC and store it
- 04 remove 'rowid' references (that are sqlite-specific); may involve server
- 04 look at other examples for better UI friend.tech
- 01 make the prod build copy the sw_scripts
- .5 Add start date to project - .5 Add start date to project
- .3 check that Android shows "back" buttons on screens without bottom tray - .3 check that Android shows "back" buttons on screens without bottom tray
- .1 Make give description text box into something that expands as they type? - .1 Make give description text box into something that expands as they type?
@@ -52,16 +44,12 @@ tasks:
- .2 Show a warning if both giver and recipient are the same (but still allow?) - .2 Show a warning if both giver and recipient are the same (but still allow?)
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui - 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker - .5 Display a more appealing confirmation on the map when erasing the marker
- .5 include the hash of the latest commit on help page next to version (maybe Trent's git-hash branch)
- .5 remove references to localStorage for projectId (now that it's pulling from the path) - .5 remove references to localStorage for projectId (now that it's pulling from the path)
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
- switch some checks for activeDid to check for isRegistered - switch some checks for activeDid to check for isRegistered
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show" - .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30) - .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
- warn if they're using the web (android only?)
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
https://web.dev/articles/get-installed-related-apps
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
- contacts v+ : - contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings). - 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 show error to user when adding a duplicate contact - .2 show error to user when adding a duplicate contact
@@ -81,7 +69,6 @@ tasks:
- 24 Move to Vite - 24 Move to Vite
- 32 accept images for projects - 32 accept images for projects
- 32 accept images for contacts - 32 accept images for contacts
- import project interactions from GitHub/GitLab and manage signing
- linking between projects or plans : - linking between projects or plans :
- show total time given to & from a project - show total time given to & from a project
@@ -104,8 +91,6 @@ tasks:
- automated tests, eg. cypress - automated tests, eg. cypress
- Notifications (wake on the phone, push notifications) - 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 - Connect with phone contacts
@@ -118,11 +103,12 @@ tasks:
- Do we want split first name & last name? - 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. - 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
- 16 From the home screen, make the quick action even easier. - 16 From the home screen, make the quick action even easier.
- allow some gives even if they aren't registered - maybe someday as a gift to the world, but we really want this to be built via personal connections
log: log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29 - videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27 - project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 215 B

View File

@@ -169,12 +169,20 @@
> >
Turn on Notifications Turn on Notifications
</button> </button>
<button <div class="grid grid-cols-2 gap-2">
@click="close(notification.id)" <button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" @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> 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> </div>
</div> </div>
@@ -230,10 +238,6 @@
</p> </p>
<button <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" 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 Turn Off Notifications
@@ -281,7 +285,7 @@ interface VapidResponse {
}; };
} }
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@@ -302,7 +306,7 @@ export default class App extends Vue {
try { try {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl = DEFAULT_PUSH_SERVER; let pushUrl: string = AppString.DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) { if (settings?.webPushServer) {
pushUrl = settings.webPushServer; pushUrl = settings.webPushServer;
} }
@@ -328,20 +332,16 @@ export default class App extends Vue {
); );
} }
} catch (error) { } catch (error) {
if (window.location.host.startsWith("localhost")) { console.error("Got an error initializing notifications:", error);
console.log("Ignoring the error getting VAPID for local development."); this.$notify(
} else { {
console.error("Got an error initializing notifications:", error); group: "alert",
this.$notify( type: "danger",
{ title: "Error Setting Notifications",
group: "alert", text: "Got an error setting notifications.",
type: "danger", },
title: "Error Setting Notifications", -1,
text: "Got an error setting notifications.", );
},
-1,
);
}
} }
} }
@@ -370,7 +370,6 @@ export default class App extends Vue {
} }
private askPermission(): Promise<NotificationPermission> { private askPermission(): Promise<NotificationPermission> {
console.log("Requesting permission for notifications:", navigator);
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) { if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
return Promise.reject("Service worker not available."); return Promise.reject("Service worker not available.");
} }
@@ -411,17 +410,14 @@ export default class App extends Vue {
private requestNotificationPermission(): Promise<NotificationPermission> { private requestNotificationPermission(): Promise<NotificationPermission> {
return Notification.requestPermission().then((permission) => { return Notification.requestPermission().then((permission) => {
if (permission !== "granted") { if (permission !== "granted") {
alert( alert("We need notification permission to provide certain features.");
"Allow this app permission to make notifications for personal reminders." +
" You can adjust them at any time in your settings.",
);
throw new Error("We weren't granted permission."); throw new Error("We weren't granted permission.");
} }
return permission; return permission;
}); });
} }
public async turnOnNotifications() { async turnOnNotifications() {
return this.askPermission() return this.askPermission()
.then((permission) => { .then((permission) => {
console.log("Permission granted:", permission); console.log("Permission granted:", permission);
@@ -443,38 +439,23 @@ export default class App extends Vue {
} }
}) })
.then(() => { .then(() => {
console.log( console.log("Subscription data sent to server.");
"Subscription data sent to server and all finished successfully.",
);
this.$notify(
{
group: "alert",
type: "success",
title: "Notifications Turned On",
text: "Notifications are on. You should see one on your device; if not, see the 'Troubleshoot' page.",
},
-1,
);
}) })
.catch((error) => { .catch((error) => {
console.error( console.error(
"Subscription or server communication failed:", "Subscription or server communication failed:",
error, error,
); );
alert( alert( "Subscription or server communication failed. Try again in a while." );
"Subscription or server communication failed. Try again in a while.",
);
}); });
}) })
.catch((error) => { .catch((error) => {
console.error( console.error("An error occurred:", error);
"An error occurred setting notification permissions:", alert( "Some error occurred." + error );
error,
);
alert("Some error occurred setting notification permissions.");
}); });
} }
private urlBase64ToUint8Array(base64String: string): Uint8Array { private urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding) const base64 = (base64String + padding)
@@ -518,7 +499,11 @@ export default class App extends Vue {
resolve(); resolve();
}) })
.catch((error) => { .catch((error) => {
console.error("Push subscription failed:", error, options); console.error(
"Subscription or server communication failed:",
error,
options,
);
// Inform the user about the issue // Inform the user about the issue
alert( alert(
@@ -534,7 +519,7 @@ export default class App extends Vue {
private sendSubscriptionToServer( private sendSubscriptionToServer(
subscription: PushSubscription, subscription: PushSubscription,
): Promise<void> { ): Promise<void> {
console.log("About to send subscription...", subscription); console.log(subscription);
return fetch("/web-push/subscribe", { return fetch("/web-push/subscribe", {
method: "POST", method: "POST",
headers: { headers: {
@@ -549,49 +534,12 @@ export default class App extends Vue {
}); });
} }
async turnOffNotifications() { never(ID: string) {
let subscription; alert(ID);
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;
});
const pushServerSuccess = await fetch("/web-push/unsubscribe", { maybeLater(ID: string) {
method: "POST", alert(ID);
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subscription),
})
.then((response) => {
return response.ok;
})
.catch((error) => {
console.log("Push server communication failed:", error);
return false;
});
alert(
"Notifications are off. Push provider unsubscribe " +
(pushProviderSuccess ? "succeeded" : "failed") +
(pushProviderSuccess === pushServerSuccess ? " and" : " but") +
" push server unsubscribe " +
(pushServerSuccess ? "succeeded" : "failed") +
".",
);
} }
} }
</script> </script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

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"> <div class="flex flex-row">
<span <span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2" 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 <div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()" @click="decrement()"
v-if="amountInput !== '0'"
> >
<fa icon="chevron-left" /> <fa icon="chevron-left" />
</div> </div>
<input <input
type="text" type="text"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center" class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="amountInput" v-model="hours"
/> />
<div <div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" 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 = ""; activeDid = "";
apiServer = ""; apiServer = "";
amountInput = "0";
giver?: GiverInputInfo; // undefined means no identified giver agent giver?: GiverInputInfo; // undefined means no identified giver agent
description = ""; description = "";
givenToUser = false; givenToUser = false;
unitCode = "HUR"; hours = "0";
visible = false; 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() { async created() {
try { try {
await db.open(); await db.open();
@@ -123,7 +101,9 @@ export default class GiftedDialog extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: err.message || "There was an error retrieving your settings.", text:
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );
@@ -135,7 +115,7 @@ export default class GiftedDialog extends Vue {
this.giver = giver; this.giver = giver;
// if we show "given to user" selection, default checkbox to true // if we show "given to user" selection, default checkbox to true
this.givenToUser = this.showGivenToUser; this.givenToUser = this.showGivenToUser;
this.amountInput = "0"; this.hours = "0";
this.visible = true; this.visible = true;
} }
@@ -145,21 +125,12 @@ export default class GiftedDialog extends Vue {
this.visible = false; 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() { increment() {
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`; this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
} }
decrement() { decrement() {
this.amountInput = `${Math.max( this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
0,
(parseFloat(this.amountInput) || 1) - 1,
)}`;
} }
cancel() { cancel() {
@@ -171,7 +142,7 @@ export default class GiftedDialog extends Vue {
this.description = ""; this.description = "";
this.giver = undefined; this.giver = undefined;
this.givenToUser = this.showGivenToUser; this.givenToUser = this.showGivenToUser;
this.amountInput = "0"; this.hours = "0";
} }
async confirm() { async confirm() {
@@ -189,8 +160,7 @@ export default class GiftedDialog extends Vue {
await this.recordGive( await this.recordGive(
this.giver?.did as string | undefined, this.giver?.did as string | undefined,
this.description, this.description,
parseFloat(this.amountInput), parseFloat(this.hours),
this.unitCode,
).then(() => { ).then(() => {
this.eraseValues(); this.eraseValues();
}); });
@@ -216,13 +186,12 @@ export default class GiftedDialog extends Vue {
* *
* @param giverDid may be null * @param giverDid may be null
* @param description may be an empty string * @param description may be an empty string
* @param amountInput may be 0 * @param hours may be 0
*/ */
public async recordGive( public async recordGive(
giverDid?: string, giverDid?: string,
description?: string, description?: string,
amountInput?: number, hours?: number,
unitCode?: string,
) { ) {
if (!this.activeDid) { if (!this.activeDid) {
this.$notify( this.$notify(
@@ -237,15 +206,13 @@ export default class GiftedDialog extends Vue {
return; return;
} }
if (!description && !amountInput) { if (!description && !hours) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: `You must enter a description or some number of ${ text: "You must enter a description or some number of hours.",
this.UNIT_LONG[this.unitCode]
}.`,
}, },
-1, -1,
); );
@@ -261,8 +228,7 @@ export default class GiftedDialog extends Vue {
giverDid, giverDid,
this.givenToUser ? this.activeDid : undefined, this.givenToUser ? this.activeDid : undefined,
description, description,
amountInput, hours,
unitCode,
this.projectId, this.projectId,
); );

View File

@@ -105,7 +105,9 @@ export default class OfferDialog extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: err.message || "There was an error retrieving your settings.", text:
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );

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 &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message = "You're linked to a non-prod server, user " + didPrefix;
} else if (
settings?.warnIfProdServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message =
"You're linked to the production server, user " + didPrefix;
}
} catch (err: unknown) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Detecting Server",
text: JSON.stringify(err),
},
-1,
);
}
}
}
</script>

View File

@@ -4,20 +4,21 @@
* See also ../libs/veramo/setup.ts * See also ../libs/veramo/setup.ts
*/ */
export enum AppString { export enum AppString {
APP_NAME = "Time Safari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch", TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000", LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
PROD_PUSH_SERVER = "https://timesafari.app", PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app", TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com", TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
} }
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host;
/** /**
* The possible values for "group" and "type" are in App.vue. * The possible values for "group" and "type" are in App.vue.
* From the notiwind package * From the notiwind package

View File

@@ -1,20 +1,18 @@
import BaseDexie, { Table } from "dexie"; import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon"; import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import { Account, AccountsSchema } from "./tables/accounts"; import { Account, AccountsSchema } from "./tables/accounts";
import { Contact, ContactSchema } from "./tables/contacts"; import { Contact, ContactsSchema } from "./tables/contacts";
import { Log, LogSchema } from "./tables/logs";
import { import {
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
Settings, Settings,
SettingsSchema, SettingsSchema,
} from "./tables/settings"; } from "./tables/settings";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { AppString } from "@/constants/app";
// Define types for tables that hold sensitive and non-sensitive data // Define types for tables that hold sensitive and non-sensitive data
type SensitiveTables = { accounts: Table<Account> }; type SensitiveTables = { accounts: Table<Account> };
type NonsensitiveTables = { type NonsensitiveTables = {
contacts: Table<Contact>; contacts: Table<Contact>;
logs: Table<Log>;
settings: Table<Settings>; settings: Table<Settings>;
}; };
@@ -28,11 +26,7 @@ export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
const SensitiveSchemas = { ...AccountsSchema }; const SensitiveSchemas = { ...AccountsSchema };
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie; export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
const NonsensitiveSchemas = { const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema };
...ContactSchema,
...LogSchema,
...SettingsSchema,
};
// Manage the encryption key. If not present in localStorage, create and store it. // Manage the encryption key. If not present in localStorage, create and store it.
const secret = const secret =
@@ -44,14 +38,15 @@ encrypted(accountsDB, { secretKey: secret });
// Define the schema for our databases // Define the schema for our databases
accountsDB.version(1).stores(SensitiveSchemas); accountsDB.version(1).stores(SensitiveSchemas);
// v1 was contacts & settings db.version(1).stores(NonsensitiveSchemas);
// v2 added logs
db.version(2).stores(NonsensitiveSchemas);
// Event handler to initialize the non-sensitive database with default settings // Event handler to initialize the non-sensitive database with default settings
db.on("populate", () => { db.on("populate", () => {
db.settings.add({ db.settings.add({
id: MASTER_SETTINGS_KEY, id: MASTER_SETTINGS_KEY,
apiServer: DEFAULT_ENDORSER_API_SERVER, 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

@@ -6,6 +6,6 @@ export interface Contact {
registered?: boolean; registered?: boolean;
} }
export const ContactSchema = { export const ContactsSchema = {
contacts: "&did, name, publicKeyBase64, registered, seesMe", contacts: "&did, name, publicKeyBase64, registered, seesMe",
}; };

View File

@@ -1,10 +0,0 @@
export interface Log {
message: string;
}
export const LogSchema = {
// Currently keyed by "date" because A) today's log data is what we need so we append, and
// B) we don't want it to grow so we remove everything if this is the first entry today.
// See safari-notifications.js logMessage for the associated logic.
logs: "date, message",
};

View File

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

View File

@@ -12,8 +12,6 @@ export const SERVICE_ID = "endorser.ch";
export const CONTACT_URL_PREFIX = "https://endorser.ch"; export const CONTACT_URL_PREFIX = "https://endorser.ch";
// the suffix for the contact URL // the suffix for the contact URL
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt="; 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 { export interface AgreeVerifiableCredential {
"@context": string; "@context": string;
@@ -40,24 +38,14 @@ export interface ClaimResult {
error: { code: string; message: string }; error: { code: string; message: string };
} }
export interface GenericVerifiableCredential { export interface GenericClaim {
"@context": string; "@context": string;
"@type": string; "@type": string;
} issuedAt: string;
// "any" because arbitrary objects can be subject of agreement
export interface GenericServerRecord extends GenericVerifiableCredential {
handleId?: string;
id?: string;
issuedAt?: string;
issuer?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>; claim: Record<any, any>;
} }
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "",
claim: {},
};
export interface GiveServerRecord { export interface GiveServerRecord {
agentDid: string; agentDid: string;
@@ -155,104 +143,6 @@ export function isHiddenDid(did: string) {
return did === HIDDEN_DID; 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 always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
@@ -310,7 +200,6 @@ export async function createAndSubmitGive(
toDid?: string, toDid?: string,
description?: string, description?: string,
hours?: number, hours?: number,
unitCode?: string,
fulfillsProjectHandleId?: string, fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
@@ -319,15 +208,13 @@ export async function createAndSubmitGive(
recipient: toDid ? { identifier: toDid } : undefined, recipient: toDid ? { identifier: toDid } : undefined,
agent: fromDid ? { identifier: fromDid } : undefined, agent: fromDid ? { identifier: fromDid } : undefined,
description: description || undefined, description: description || undefined,
object: hours object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
? { amountOfThisGood: hours, unitCode: unitCode || "HUR" }
: undefined,
fulfills: fulfillsProjectHandleId fulfills: fulfillsProjectHandleId
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId } ? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
: undefined, : undefined,
}; };
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericServerRecord, vcClaim as GenericClaim,
identity, identity,
apiServer, apiServer,
axios, axios,
@@ -375,7 +262,7 @@ export async function createAndSubmitOffer(
}; };
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericServerRecord, vcClaim as GenericClaim,
identity, identity,
apiServer, apiServer,
axios, axios,
@@ -383,7 +270,7 @@ export async function createAndSubmitOffer(
} }
export async function createAndSubmitClaim( export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential, vcClaim: GenericClaim,
identity: IIdentifier, identity: IIdentifier,
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
@@ -486,10 +373,6 @@ export interface ProjectData {
* URL referencing information about the project * URL referencing information about the project
**/ **/
handleId: string; handleId: string;
/**
* The DID of the issuer
*/
issuerDid: string;
/** /**
* The Identier of the project * The Identier of the project
**/ **/

View File

@@ -1,5 +1,3 @@
// many of these are also found in endorser-mobile utility.ts
export const isGlobalUri = (uri: string) => { export const isGlobalUri = (uri: string) => {
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
}; };

View File

@@ -13,7 +13,6 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { import {
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faArrowUpRightFromSquare,
faBan, faBan,
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,
@@ -44,7 +43,6 @@ import {
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
faMagnifyingGlass, faMagnifyingGlass,
faMessage,
faPen, faPen,
faPersonCircleCheck, faPersonCircleCheck,
faPersonCircleQuestion, faPersonCircleQuestion,
@@ -66,7 +64,6 @@ import {
library.add( library.add(
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faArrowUpRightFromSquare,
faBan, faBan,
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,
@@ -97,7 +94,6 @@ library.add(
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
faMagnifyingGlass, faMagnifyingGlass,
faMessage,
faPen, faPen,
faPersonCircleCheck, faPersonCircleCheck,
faPersonCircleQuestion, faPersonCircleQuestion,

View File

@@ -39,12 +39,7 @@ const routes: Array<RouteRecordRaw> = [
name: "account", name: "account",
component: () => component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"), import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
}, beforeEnter: enterOrStart,
{
path: "/claim/:id?",
name: "claim",
component: () =>
import(/* webpackChunkName: "claim" */ "../views/ClaimView.vue"),
}, },
{ {
path: "/confirm-contact", 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> <template>
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile"></QuickNav>
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
@@ -34,24 +32,8 @@
</span> </span>
</div> </div>
<!-- ID notice -->
<div
v-if="!activeDid"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Note:</b> Before you can take any action, you need an ID.
</p>
<router-link
:to="{ name: 'start' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
>
Generate Identity
</router-link>
</div>
<!-- Registration notice --> <!-- Registration notice -->
<!-- We won't show any loading indicator 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 <div
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime" v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4" class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
@@ -76,9 +58,9 @@
<span v-else> <span v-else>
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
class="block w-full text-center text-md text-slate-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2" class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
> >
(Set Your Name) (set name)
</router-link> </router-link>
</span> </span>
@@ -105,10 +87,18 @@
</router-link> </router-link>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<div <label
v-if="!notificationMaybeChanged" for="toggleNotifications"
class="flex items-center justify-between cursor-pointer" class="flex items-center cursor-pointer"
@click="showNotificationChoice()" @click="
this.$notify(
{
group: 'modal',
type: 'notification-permission',
},
-1,
)
"
> >
<!-- label --> <!-- label -->
<div>App Notifications</div> <div>App Notifications</div>
@@ -117,8 +107,8 @@
<!-- input --> <!-- input -->
<input <input
type="checkbox" type="checkbox"
v-model="isSubscribed" v-model="toggleNotifications"
name="toggleNotificationsInput" name="toggleNotifications"
class="sr-only" class="sr-only"
/> />
<!-- line --> <!-- line -->
@@ -128,21 +118,46 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div> ></div>
</div> </div>
</div> </label>
<div v-else> <label
Notification status may have changed. Revisit this page to see the for="toggleMuteNotifications"
latest setting. class="flex items-center cursor-pointer mt-4"
</div> @click="
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications"> this.$notify(
Test your notification setup. {
</router-link> 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> </div>
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3> <h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
<router-link <router-link
:to="{ name: 'seed-backup' }" :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" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
> >
Backup Identifier Seed Backup Identifier Seed
@@ -157,42 +172,30 @@
</a> </a>
<a ref="downloadLink" /> <a ref="downloadLink" />
<div v-if="activeDid" class="my-8"> <div v-if="activeDid" class="flex py-2">
<h3 class="text-sm uppercase font-semibold mb-3">Rate Limits</h3> <button class="text-center text-md text-blue-500" @click="checkLimits()">
<button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
@click="checkLimits()"
>
Check Limits Check Limits
</button> </button>
<!-- show spinner if loading limits --> <!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center mb-4"> <div v-if="loadingLimits" class="ml-2">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa> Checking... <fa icon="spinner" class="fa-spin"></fa>
</div> </div>
<div class="mb-4"> <div class="ml-2">
{{ limitsMessage }} {{ limitsMessage }}
</div> </div>
<div v-if="!!limits?.nextWeekBeginDateTime"> <div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<p class="mb-3 text-sm"> <span class="font-bold">Rate Limits</span>
You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of <p>
<b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims You have done {{ limits.doneClaimsThisWeek }} claims out of
counter resets at {{ limits.maxClaimsPerWeek }} for this week. Your claims counter
<b class="whitespace-nowrap">{{ resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
readableTime(limits.nextWeekBeginDateTime)
}}</b>
</p> </p>
<p class="text-sm"> <p>
You have done You have done {{ limits.doneRegistrationsThisMonth }} registrations
<b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of out of {{ limits.maxRegistrationsPerMonth }} for this month. (You can
<b>{{ limits.maxRegistrationsPerMonth }}</b> for this month. register nobody on your first day, and after that only one a day in
<i your first month.) Your registration counter resets at
>(You can register nobody on your first day, and after that only one {{ readableTime(limits.nextMonthBeginDateTime) }}
a day in your first month.)</i
>
Your registration counter resets at
<b class="whitespace-nowrap">
{{ readableTime(limits.nextMonthBeginDateTime) }}
</b>
</p> </p>
</div> </div>
</div> </div>
@@ -205,14 +208,10 @@
> >
Advanced Advanced
</h3> </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 --> <!-- 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 Deep Identity Details
</h2> </h2>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
@@ -270,11 +269,13 @@
<label <label
for="toggleShowAmounts" for="toggleShowAmounts"
class="flex items-center justify-between cursor-pointer my-4" class="flex items-center cursor-pointer py-2"
@click="handleChange" @click="handleChange"
> >
<!-- label --> <!-- 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 --> <!-- toggle -->
<div class="relative ml-2"> <div class="relative ml-2">
<!-- input --> <!-- input -->
@@ -293,21 +294,26 @@
</div> </div>
</label> </label>
<router-link <div class="flex py-2">
:to="{ name: 'statistics' }" <button class="text-blue-500">
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" <router-link :to="{ name: 'statistics' }" class="block text-center">
> See Global Animated History of Giving
See Animated Global History of Giving </router-link>
</router-link> </button>
</div>
<!-- id used by puppeteer test script --> <div class="flex py-2">
<router-link <button class="text-blue-500">
id="switch-identity-link" <!-- id used by puppeteer test script -->
:to="{ name: 'identity-switcher' }" <router-link
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" id="switch-identity-link"
> :to="{ name: 'identity-switcher' }"
Switch Identity class="block text-center"
</router-link> >
Switch Identity / No Identity
</router-link>
</button>
</div>
<div class="flex py-4"> <div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2> <h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
@@ -343,46 +349,6 @@
</button> </button>
</div> </div>
<label
for="toggleProdWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleProdWarning"
>
<!-- label -->
<h2>Show warning if on prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<label
for="toggleTestWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleTestWarning"
>
<!-- label -->
<h2>Show warning if on non-prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<div class="flex py-4"> <div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2"> <h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server Notification Push Server
@@ -434,7 +400,6 @@ import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@@ -459,7 +424,7 @@ interface IAccount {
derivationPath: string; derivationPath: string;
} }
@Component({ components: { QuickNav, TopMessage } }) @Component({ components: { QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@@ -471,8 +436,6 @@ export default class AccountViewView extends Vue {
derivationPath = ""; derivationPath = "";
givenName = ""; givenName = "";
isRegistered = false; isRegistered = false;
isSubscribed = false;
notificationMaybeChanged = false;
numAccounts = 0; numAccounts = 0;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
@@ -490,63 +453,14 @@ export default class AccountViewView extends Vue {
showAdvanced = false; showAdvanced = false;
subscription: PushSubscription | null = null; private isSubscribed = false;
warnIfProdServer = false;
warnIfTestServer = false;
/** get toggleNotifications() {
* Async function executed when the component is created. return this.isSubscribed;
* 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() { set toggleNotifications(value) {
try { this.isSubscribed = value;
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription;
} catch (error) {
console.error("Mount error:", error);
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline;
this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
} }
public async getIdentity(activeDid: string): Promise<IIdentifier | null> { public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
@@ -614,16 +528,6 @@ export default class AccountViewView extends Vue {
this.updateShowContactAmounts(); this.updateShowContactAmounts();
} }
toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer;
this.updateWarnIfProdServer(this.warnIfProdServer);
}
toggleTestWarning() {
this.warnIfTestServer = !this.warnIfTestServer;
this.updateWarnIfTestServer(this.warnIfTestServer);
}
readableTime(timeStr: string) { readableTime(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T")); return timeStr.substring(0, timeStr.indexOf("T"));
} }
@@ -633,6 +537,62 @@ export default class AccountViewView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); 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. * Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information. * @param {IdentityType} identity - Object containing identity information.
@@ -657,31 +617,6 @@ export default class AccountViewView extends Vue {
} }
} }
async showNotificationChoice() {
if (!this.subscription) {
this.$notify(
{
group: "modal",
type: "notification-permission",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
} else {
this.$notify(
{
group: "modal",
type: "notification-off",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
}
this.notificationMaybeChanged = true;
}
/** /**
* Handles errors and updates the component's state accordingly. * Handles errors and updates the component's state accordingly.
* @param {Error} err - The error object. * @param {Error} err - The error object.
@@ -720,58 +655,12 @@ export default class AccountViewView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error Updating Contact Setting", title: "Error Updating Contact Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.", text: "Clear your cache and start over (after data backup).",
}, },
-1, -1,
); );
console.error( console.error(
"Telling user to try again after contact setting update because:", "Telling user to clear cache after contact setting update because:",
err,
);
}
}
public async updateWarnIfProdServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Prod Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after setting update because:",
err,
);
}
}
public async updateWarnIfTestServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Test Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after setting update because:",
err, err,
); );
} }
@@ -1034,15 +923,6 @@ export default class AccountViewView extends Vue {
webPushServer: this.webPushServerInput, webPushServer: this.webPushServerInput,
}); });
this.webPushServer = this.webPushServerInput; this.webPushServer = this.webPushServerInput;
this.$notify(
{
group: "alert",
type: "warning",
title: "Reload",
text: "Now reload the app to get a new VAPID to use with this push server.",
},
-1,
);
} }
} }
</script> </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

@@ -25,13 +25,6 @@
<span class="justify-around">(Only 50 most recent)</span> <span class="justify-around">(Only 50 most recent)</span>
<span /> <span />
</div> </div>
<div class="flex justify-around">
<span />
<span class="justify-around">
(This does not include claims by them if they're not visible to you.)
</span>
<span />
</div>
<!-- Results List --> <!-- Results List -->
<table <table
@@ -131,7 +124,7 @@ interface Notification {
} }
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue { export default class ContactsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
@@ -185,7 +178,6 @@ export default class ContactAmountssView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings or gives.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -193,7 +185,7 @@ export default class ContactAmountssView extends Vue {
title: "Error", title: "Error",
text: text:
err.userMessage || err.userMessage ||
"There was an error retrieving your settings and/or contacts and/or gives.", "There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );

View File

@@ -145,7 +145,6 @@ export default class ContactGiftingView extends Vue {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings & contacts:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -153,7 +152,7 @@ export default class ContactGiftingView extends Vue {
title: "Error", title: "Error",
text: text:
err.message || err.message ||
"There was an error retrieving your settings and/or contacts.", "There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );

View File

@@ -18,16 +18,6 @@
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info Your Contact Info
</h1> </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>
<div @click="onCopyToClipboard()" v-if="activeDid"> <div @click="onCopyToClipboard()" v-if="activeDid">
@@ -97,7 +87,6 @@ export default class ContactQRScanShow extends Vue {
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
givenName = "";
qrValue = ""; qrValue = "";
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
@@ -122,7 +111,6 @@ export default class ContactQRScanShow extends Vue {
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.givenName = settings?.firstName || "";
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();

View File

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

View File

@@ -1,6 +1,5 @@
<template> <template>
<QuickNav selected="Discover"></QuickNav> <QuickNav selected="Discover"></QuickNav>
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
@@ -137,7 +136,6 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
interface Notification { interface Notification {
group: string; group: string;
@@ -151,7 +149,6 @@ interface Notification {
QuickNav, QuickNav,
InfiniteScroll, InfiniteScroll,
EntityIcon, EntityIcon,
TopMessage,
}, },
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
@@ -277,8 +274,8 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data; const plans: ProjectData[] = results.data;
if (plans) { if (plans) {
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, issuerDid, rowid } = plan; const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, issuerDid, rowid }); this.projects.push({ name, description, handleId, rowid });
} }
this.remoteCount = this.projects.length; this.remoteCount = this.projects.length;
} else { } else {
@@ -360,14 +357,8 @@ export default class DiscoverView extends Vue {
if (beforeId) { if (beforeId) {
const plans: ProjectData[] = results.data; const plans: ProjectData[] = results.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, issuerDid, rowid } = plan; const { name, description, handleId = plan.handleId, rowid } = plan;
this.projects.push({ this.projects.push({ name, description, handleId, rowid });
name,
description,
handleId,
issuerDid,
rowid,
});
} }
} else { } else {
this.projects = results.data; this.projects = results.data;

View File

@@ -22,219 +22,34 @@
</div> </div>
<div> <div>
<p>Here are ways to test notifications and get them working.</p> <p>Here are things to try to get notifications working.</p>
<h2 class="text-xl font-semibold">Full Test</h2> <h2 class="text-xl font-semibold">Test</h2>
<div> <p>Somehow call the service-worker self.showNotification</p>
<p>
If this works then you're all set.
<button
@click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server but
Skipping Client Filter)
</button>
</p>
</div>
<h2 class="text-xl font-semibold">If this app is not installed...</h2> <h2 class="text-xl font-semibold">Check OS-level permissions</h2>
<div> <p>
<p> Walk-throughs & screenshots, maybe for all combinations of OS &
For best results on mobile, install this app on your device (as browsers.
opposed to using it inside the broser app). In Chrome, it may prompt </p>
you, and you can also look for the "Install" command in the browser
settings; on the the deskop, look for this icon in the address bar:
<img
src="../assets/help/chrome-install-pwa.png"
alt="Chrome 'install' icon"
class="ml-4"
/>
</p>
</div>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">Check browser-level permissions</h2>
If "you must enable notifications"... <p>Walk-throughs & screenshots for browser settings</p>
</h2>
<div>
<p>
Wait for about 10 seconds (for the service worker to activate), then
<button class="text-blue-500" @click="showNotificationChoice()">
click here.
</button>
</p>
</div>
<h2 class="text-xl font-semibold">Check App Permissions</h2> <h2 class="text-xl font-semibold">Explain full reset to start again</h2>
<div> <p>
<p> Walk-throughs for clearing everything & subscribing anew to get a
In Apple iOS, check "Settings" -> "Notifications", look for the Time message
Safari app (or the browser you're using), and make sure notifications </p>
are enabled.
</p>
<p>
In Android, hold on to the app icon, then select "App Info", then
"Notifications" and make sure they're enabled. If it's still a problem
then go further:
</p>
<p>
If you installed the app with Chrome, make sure there are no other
tabs with it open. Here are some ways to clear caches that can mess
things up (and note that this clears out data from the installed app
-- which is good to do while the app is installed):
</p>
<ul>
<li class="list-disc ml-4">
Go to Chrome "App Info", then "Storage & Cache" and "Clear Storage".
</li>
<li class="list-disc ml-4">
Go to Chrome "Settings", then "Privacy and Security" and "Clear
"Clear browsing data", then "Cookies and site data". Also make sure
the "Time Range" at the top shows "All time".
</li>
</ul>
<p>
On a Mac, go to "Settings" and check "Notifications".
<img
src="../assets/help/mac-installed-app-settings.png"
alt="Mac app settings"
class="ml-4"
/>
</p>
</div>
<h2 class="text-xl font-semibold">Check Browser Permissions</h2> <h2 class="text-xl font-semibold">Auto-detection</h2>
<div> <p>Show results of auto-detection whether they're turned on</p>
<p>In Apple iOS, check Settings -> Notifications.</p>
<p>In Android, check Settings -> Notifications.</p>
You can find more details about compatibility
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
class="text-blue-500"
target="_blank"
>
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
<h2 class="text-xl font-semibold">Check OS Permissions</h2>
<div class="px-2">
<div>
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
<div>
Notifications require iOS 16.4 or higher. To check your iOS version,
go to Settings > General > About > Software Version.
</div>
<h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3>
<div>
We recommend Chrome. It must be version 42 or higher. Check your
version under Settings -> About Chrome.
</div>
<h3 class="text-lg font-semibold">Desktop - Mac</h3>
<div>
<span>
Requires Mac OS 13; see your macOS version under Apple -> About
This Mac.
</span>
</div>
<h3 class="text-lg font-semibold">Windows desktop</h3>
In Windows, check Settings -> Notifications.
<img
src="../assets/help/windows-system-enable-notifications.png"
alt="Windows system settings"
class="ml-4"
/>
</div>
<div>
You can find more details about compatibility
<a
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
class="text-blue-500"
target="_blank"
>
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
</div>
<h2 class="text-xl font-semibold">Reinstall</h2>
<div>
<p>
If all else fails, uninstall the app, ensure all the browser tabs with
it are closed, and clear out caches and storage.
</p>
<ul class="ml-4 list-disc">
<li>
Clear cache for site. (In Chrome, go to `chrome://settings/cookies`
and "all site data and permissions"; in Firefox, go to
`about:preferences` and search for "cache" then "Manage Data", and
also manually remove the IndexedDB data if the DBs still show.)
</li>
<li>
Clear notification permission. (in Chrome, go to
`chrome://settings/content/notifications`; in Firefox, go to
`about:preferences` and search for "notifications".)
</li>
<li>
Unregister service worker. (in Chrome, go to
`chrome://serviceworker-internals/`; in Firefox, go to
`about:serviceworkers`.)
</li>
<li>
Clear Cache Storage. (in Chrome, in dev tools under Application; in
Firefox, in dev tools under Storage.)
</li>
</ul>
<p>Then reinstall the app.</p>
</div>
<button
@click="showTestNotification()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Test Notification Directly to Device (Not Through Push Server)
</button>
<button
@click="alertWebPushSubscription()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Show Web Push Subscription Info
</button>
<button
@click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server but Skipping
Client Filter)
</button>
<button
@click="sendTestWebPushMessage()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Yourself a Test Web Push Message (Through Push Server and Client
Filter)
</button>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface Notification { interface Notification {
group: string; group: string;
@@ -246,156 +61,5 @@ interface Notification {
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class HelpNotificationsView extends Vue { export default class HelpNotificationsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
subscription: PushSubscription | null = null;
async mounted() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
} catch (error) {
console.error("Mount error:", error);
}
}
alertWebPushSubscription() {
console.log(
"Web push subscription:",
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
);
alert(JSON.stringify(this.subscription));
}
async sendTestWebPushMessage(skipFilter: boolean = false) {
if (!this.subscription) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not Subscribed",
text: "You must enable notifications before testing the web push.",
},
-1,
);
return;
}
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
const auth = Buffer.from(this.subscription.getKey("auth"));
const authB64 = auth
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const p256dh = Buffer.from(this.subscription.getKey("p256dh"));
const p256dhB64 = p256dh
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const newPayload = {
endpoint: this.subscription.endpoint,
keys: {
auth: authB64,
p256dh: p256dhB64,
},
message: `Test, where you will see this message ${
skipFilter ? "un" : ""
}filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
};
console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload);
const response = await axios.post(
pushUrl + "/web-push/send-test",
payloadStr,
{
headers: {
"Content-Type": "application/json",
},
},
);
console.log("Got response from web push server:", response);
this.$notify(
{
group: "alert",
type: "success",
title: "Test Web Push Sent",
text: "Check your device for the test web push message, depending on the filtering you chose.",
},
-1,
);
} catch (error) {
console.error("Got an error sending test notification:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Sending Test",
text: "Got an error sending the test web push notification.",
},
-1,
);
}
}
showTestNotification() {
const TEST_NOTIFICATION_TITLE = "It Worked";
navigator.serviceWorker.ready
.then((registration) => {
return registration.showNotification(TEST_NOTIFICATION_TITLE, {
body: "This is your test notification.",
});
})
.then(() => {
this.$notify(
{
group: "alert",
type: "success",
title: "Sent",
text: `A notification was triggered, so one should show on your device entitled '${TEST_NOTIFICATION_TITLE}'.`,
},
5000,
);
})
.catch((error) => {
console.error("Got a notification error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Failed",
text: "Got an error sending a notification.",
},
-1,
);
});
}
showNotificationChoice() {
this.$notify(
{
group: "modal",
type: "notification-permission",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
}
} }
</script> </script>

View File

@@ -30,17 +30,16 @@
<h2 class="text-xl font-semibold">What is the philosophy here?</h2> <h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<p> <p>
We are building networks of people who want to grow a giving society. We are building networks of people who want to grow a giving society.
First of all, you can see what people have given, and also recognize First of all, you can record ways you've seen people give, and that
gifts you've seen, in a way that leaves a permanent record -- one that leaves a permanent record -- one that came from you, and the recipient
came from you, and the recipient can prove it was for them. This is can prove it was for them. This is personally gratifying, but it extends
personally gratifying, but it extends to broader work: volunteers get to broader work: volunteers can get confirmation of activity and
confirmation of activity, and selectively show off their contributions selectively show off their contributions and network.
and network.
</p> </p>
<p> <p>
You can show giving and also offer help to ideas, based on others' You can also record projects and plans and invite others to collaborate.
willingness to help out, too. You can record your own ideas and invite Soon you'll be able to see when others are interested and see how much
others to collaborate. they're willing to contribute, even if there are conditions.
</p> </p>
<p> <p>
This app uses the power of cryptography to build a reputation, recording This app uses the power of cryptography to build a reputation, recording
@@ -182,15 +181,6 @@
different page. different page.
</p> </p>
<h2 class="text-xl font-semibold">
Where do I get help with notifications?
</h2>
<p>
<router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link
>
</p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I access even more functionality? How do I access even more functionality?
</h2> </h2>
@@ -224,7 +214,9 @@
</p> </p>
<h2 class="text-xl font-semibold">What app version is this?</h2> <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"> <h2 class="text-xl font-semibold">
For any other questions, including removing your data: For any other questions, including removing your data:
@@ -254,7 +246,6 @@ export default class Help extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
package = Package; package = Package;
commitHash = process.env.VUE_APP_GIT_HASH;
showOnboardInfo() { showOnboardInfo() {
this.$notify( this.$notify(

View File

@@ -1,7 +1,5 @@
<template> <template>
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
@@ -10,59 +8,28 @@
<!-- show the actions for recognizing a give --> <!-- show the actions for recognizing a give -->
<div class="mb-8"> <div class="mb-8">
<div <div v-if="!activeDid">
v-if="!isInstalled()" To record others' giving,
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" <router-link :to="{ name: 'start' }" class="text-blue-500">
> create your identifier.</router-link
<p>
You should install this as an app.
<router-link
:to="{ name: 'help-notifications' }"
class="text-blue-500"
>
Go here for instructions.
</router-link>
</p>
</div>
<div
v-if="!activeDid"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<p class="text-lg mb-3">
You need an <b>identifier</b> before you can record anyone's gives.
</p>
<router-link
:to="{ name: 'start' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
Create Your Identifier</router-link
> >
</div> </div>
<div <div v-else-if="!isRegistered">
v-else-if="!isRegistered" To record others' giving, someone must register your account, so show
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" them
> <router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
Someone must register your account before you can record anyone's gives. your identity info</router-link
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"
> >
1. Show Them Your Identity Info</router-link and then
> <router-link :to="{ name: 'account' }" class="text-blue-500">
<router-link check your limits.</router-link
:to="{ name: 'account' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
2. Check Your Limits</router-link
> >
</div> </div>
<div v-else> <div v-else>
<!-- activeDid && isRegistered --> <!-- 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"> <ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()"> <li @click="openDialog()">
@@ -120,53 +87,40 @@
showGivenToUser="true" showGivenToUser="true"
/> />
<!-- Results List -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-bold mb-4">Latest Activity</h2> <h2 class="text-xl font-bold mb-4">Latest Activity</h2>
<InfiniteScroll @reached-bottom="loadMoreGives">
<ul class="border-t border-slate-300">
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedClaimId"
>
You've seen all the following
</div>
<div class="flex">
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
<span class="">{{ this.giveDescription(record) }}</span>
<a @click="onClickLoadClaim(record.jwtId)">
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
</a>
</div>
</li>
</ul>
</InfiniteScroll>
<div :class="{ hidden: isHiddenSpinner }"> <div :class="{ hidden: isHiddenSpinner }">
<p class="text-slate-500 text-center italic mt-4 mb-4"> <p class="text-slate-500 text-center italic mt-4 mb-4">
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip; <fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</p> </p>
</div> </div>
<ul class="border-t border-slate-300">
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId"
>
You've seen all the following
</div>
<div class="flex">
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
<!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
<span class="">{{ this.giveDescription(record) }}</span>
</div>
</li>
</ul>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
@@ -174,7 +128,11 @@ import {
GiverInputInfo, GiverInputInfo,
GiveServerRecord, GiveServerRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Account } from "@/db/tables/accounts";
interface Notification { interface Notification {
group: string; group: string;
@@ -184,13 +142,7 @@ interface Notification {
} }
@Component({ @Component({
components: { components: { GiftedDialog, QuickNav, EntityIcon },
GiftedDialog,
QuickNav,
EntityIcon,
InfiniteScroll,
TopMessage,
},
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@@ -199,9 +151,10 @@ export default class HomeView extends Vue {
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
feedAllLoaded = false;
feedData = []; feedData = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedId?: string;
isHiddenSpinner = true; isHiddenSpinner = true;
isRegistered = false; isRegistered = false;
numAccounts = 0; numAccounts = 0;
@@ -241,15 +194,11 @@ export default class HomeView extends Vue {
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedId = settings?.lastViewedClaimId;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
// this returns a Promise but we don't need to wait for it
this.updateAllFeed(); this.updateAllFeed();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings and/or feed.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -257,25 +206,13 @@ export default class HomeView extends Vue {
title: "Error", title: "Error",
text: text:
err.userMessage || err.userMessage ||
"There was an error retrieving your settings and/or the latest activity.", "There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );
} }
} }
// from https://benborgers.com/posts/pwa-detect-installed
isInstalled() {
// For iOS
if ("standalone" in window.navigator) return true;
// For Android
if (window.matchMedia("(display-mode: standalone)").matches) return true;
// If neither is true, it's not installed
return false;
}
public async buildHeaders() { public async buildHeaders() {
const headers: HeadersInit = { const headers: HeadersInit = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -302,33 +239,24 @@ export default class HomeView extends Vue {
return headers; return headers;
} }
/**
* Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load
**/
public async loadMoreGives(payload: boolean) {
if (payload) {
this.updateAllFeed();
}
}
public async updateAllFeed() { public async updateAllFeed() {
this.isHiddenSpinner = false; this.isHiddenSpinner = false;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId) await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => { .then(async (results) => {
if (results.data.length > 0) { if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data); this.feedData = this.feedData.concat(results.data);
this.feedAllLoaded = results.hitLimit;
this.feedPreviousOldestId = this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId; results.data[results.data.length - 1].jwtId;
// The following update is only done on the first load.
if ( if (
this.feedLastViewedClaimId == null || this.feedLastViewedId == null ||
this.feedLastViewedClaimId < results.data[0].jwtId this.feedLastViewedId < results.data[0].jwtId
) { ) {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId, lastViewedClaimId: results.data[0].jwtId,
}); });
// but not for this page because we need to remember what it was before
} }
} }
}) })
@@ -338,22 +266,17 @@ export default class HomeView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Feed Error", title: "Export Error",
text: e.userMessage || "There was an error retrieving feed data.", text: e.userMessage || "There was an error retrieving feed data.",
}, },
-1, -1,
); );
}); });
this.isHiddenSpinner = true; this.isHiddenSpinner = true;
} }
/** public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
* Retrieve claims in reverse chronological order
*
* @param beforeId the earliest ID (of previous searches) to search earlier
* @return claims in reverse chronological order
*/
public async retrieveGives(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch( const response = await fetch(
endorserApiServer + "/api/v2/report/gives?" + beforeQuery, endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
@@ -418,13 +341,6 @@ export default class HomeView extends Vue {
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount; return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
} }
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
this.$router.push(route);
}
displayAmount(code: string, amt: number) { displayAmount(code: string, amt: number) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1); 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("firstName", this.givenName as string);
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3 localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
this.$router.back(); this.$router.push({ name: "account" });
} }
onClickCancel() { onClickCancel() {

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,9 @@
<template> <template>
<QuickNav selected="Projects"></QuickNav> <QuickNav selected="Projects"></QuickNav>
<TopMessage />
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Ideas Your Plans
</h1> </h1>
<!-- Quick Search --> <!-- Quick Search -->
@@ -81,7 +79,6 @@ import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
import { ProjectData } from "@/libs/endorserServer"; import { ProjectData } from "@/libs/endorserServer";
interface Notification { interface Notification {
@@ -92,7 +89,7 @@ interface Notification {
} }
@Component({ @Component({
components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage }, components: { InfiniteScroll, QuickNav, EntityIcon },
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@@ -125,8 +122,8 @@ export default class ProjectsView extends Vue {
if (resp.status === 200 || !resp.data.data) { if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data; const plans: ProjectData[] = resp.data.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, issuerDid, rowid } = plan; const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, issuerDid, rowid }); this.projects.push({ name, description, handleId, rowid });
} }
} else { } else {
console.log("Bad server response & data:", resp.status, resp.data); console.log("Bad server response & data:", resp.status, resp.data);

View File

@@ -4,121 +4,62 @@ importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js", "https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
); );
function logConsoleAndDb(message, arg1, arg2) { self.addEventListener("install", (event) => {
// in chrome://serviceworker-internals note that the arg1 and arg2 here will show as "[object Object]" in that page but will show as expandable objects in the console console.error("Adding event listener for:", event);
console.log(`${new Date().toISOString()} ${message}`, arg1, arg2); importScripts(
if (self.appendDailyLog) {
let fullMessage = `${new Date().toISOString()} ${message}`;
if (arg1) {
fullMessage += `\n${JSON.stringify(arg1)}`;
}
if (arg2) {
fullMessage += `\n${JSON.stringify(arg2)}`;
}
self.appendDailyLog(fullMessage);
} else {
// sometimes we get the error: "Uncaught TypeError: self.appendDailyLog is not a function"
console.log("Not logging to DB because self.appendDailyLog doesn't exist.");
}
}
self.addEventListener("install", async (event) => {
console.log("Service worker got install event. Importing scripts...", event);
await importScripts(
"safari-notifications.js", "safari-notifications.js",
"nacl.js", "nacl.js",
"noble-curves.js", "noble-curves.js",
"noble-hashes.js", "noble-hashes.js",
); );
// this should now be available
logConsoleAndDb("Service worker imported all scripts.");
});
self.addEventListener("activate", (event) => {
logConsoleAndDb("Service worker is activating...", event);
// see https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
// and https://web.dev/articles/service-worker-lifecycle#clientsclaim
event.waitUntil(clients.claim());
logConsoleAndDb("Service worker is activated.");
}); });
self.addEventListener("push", function (event) { self.addEventListener("push", function (event) {
let text = null;
if (event.data) {
text = event.data.text();
}
logConsoleAndDb("Service worker received a push event.", text, event);
event.waitUntil( event.waitUntil(
(async () => { (async () => {
try { try {
let payload; let payload;
if (text) { if (event.data) {
try { payload = JSON.parse(event.data.text());
payload = JSON.parse(text);
} catch (e) {
// don't use payload since it is not JSON
}
}
// This is a special value that tells the service worker to trigger its daily check.
// See https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/c1ed026662e754348a5f91542680bd4f57e5b81e/app.py#L217
const DAILY_UPDATE_TITLE = "DAILY_CHECK";
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
// Make sure it is something other than the DAILY_UPDATE_TITLE.
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
let title;
let message = "Got some empty message.";
if (payload && payload.title == DIRECT_PUSH_TITLE) {
// skip any search logic and show the message directly
title = "Direct Notification";
message = payload.message || "No details were provided.";
} else {
// any other title will run through regular filtering logic
if (payload && payload.title === DAILY_UPDATE_TITLE) {
title = "Daily Update";
} else {
title = payload.title || "Update";
}
message = await self.getNotificationCount();
} }
const message = await self.getNotificationCount();
if (message) { if (message) {
console.log("Will notify user:", message);
const title = payload ? payload.title : "Custom Title";
const options = { const options = {
body: message, body: message,
icon: payload ? payload.icon : "icon.png", icon: payload ? payload.icon : "icon.png",
badge: payload ? payload.badge : "badge.png", badge: payload ? payload.badge : "badge.png",
}; };
await self.registration.showNotification(title, options); await self.registration.showNotification(title, options);
logConsoleAndDb("Notified user:", options);
} else { } else {
logConsoleAndDb("No notification message."); console.log("No notification message, so will not tell the user.");
} }
} catch (error) { } catch (error) {
logConsoleAndDb("Error with push event", event, error); console.error("Error processing the push event:", error);
} }
})(), })(),
); );
}); });
self.addEventListener("message", (event) => { self.addEventListener("message", (event) => {
logConsoleAndDb("Service worker got a message...", event);
if (event.data && event.data.type === "SEND_LOCAL_DATA") { if (event.data && event.data.type === "SEND_LOCAL_DATA") {
self.secret = event.data.data; self.secret = event.data.data;
event.ports[0].postMessage({ success: true }); event.ports[0].postMessage({ success: true });
} }
logConsoleAndDb("Service worker posted a message."); });
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim());
console.log("Service worker activated", event);
}); });
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
logConsoleAndDb("Service worker got fetch event.", event); console.log("Got fetch event", event.request);
}); });
self.addEventListener("error", (event) => { self.addEventListener("error", (event) => {
logConsoleAndDb("Service worker error", event); console.error("Error in Service Worker:", event.message);
console.error("Full Error:", event);
console.error("Message:", event.message);
console.error("File:", event.filename); console.error("File:", event.filename);
console.error("Line:", event.lineno); console.error("Line:", event.lineno);
console.error("Column:", event.colno); console.error("Column:", event.colno);

View File

@@ -395,42 +395,12 @@ async function setMostRecentNotified(id) {
data["lastNotifiedClaimId"] = id; data["lastNotifiedClaimId"] = id;
await updateRecord(store, data); await updateRecord(store, data);
} else { } else {
console.error( console.error("Record not found");
"safari-notifications setMostRecentNotified IndexedDB settings record not found",
);
} }
transaction.oncomplete = () => db.close(); transaction.oncomplete = () => db.close();
} catch (error) { } catch (error) {
console.error( console.error("Database error: " + error.message);
"safari-notifications setMostRecentNotified IndexedDB error",
error,
);
}
}
async function appendDailyLog(message) {
try {
const db = await openIndexedDB("TimeSafari");
const transaction = db.transaction("logs", "readwrite");
const store = transaction.objectStore("logs");
// will only keep one day's worth of logs
const todayKey = new Date().toDateString();
const previous = await getRecord(store, todayKey);
if (!previous) {
await store.clear(); // clear out anything older than today
}
let fullMessage = (previous && previous.message) || "";
if (fullMessage) {
fullMessage += "\n";
}
fullMessage += message;
await updateRecord(store, { date: todayKey, message: fullMessage });
transaction.oncomplete = () => db.close();
return true;
} catch (error) {
console.error("safari-notifications logMessage IndexedDB error", error);
return false;
} }
} }
@@ -450,7 +420,6 @@ function getRecord(store, key) {
}); });
} }
// Note that this assumes there is only one record in the store.
function updateRecord(store, data) { function updateRecord(store, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = store.put(data); const request = store.put(data);
@@ -461,23 +430,20 @@ function updateRecord(store, data) {
async function fetchAllAccounts() { async function fetchAllAccounts() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const openRequest = indexedDB.open("TimeSafariAccounts"); let openRequest = indexedDB.open("TimeSafariAccounts");
openRequest.onupgradeneeded = function (event) { openRequest.onupgradeneeded = function (event) {
const db = event.target.result; let db = event.target.result;
if (!db.objectStoreNames.contains("accounts")) { if (!db.objectStoreNames.contains("accounts")) {
db.createObjectStore("accounts", { keyPath: "id" }); db.createObjectStore("accounts", { keyPath: "id" });
} }
if (!db.objectStoreNames.contains("worker_log")) {
db.createObjectStore("worker_log");
}
}; };
openRequest.onsuccess = function (event) { openRequest.onsuccess = function (event) {
const db = event.target.result; let db = event.target.result;
const transaction = db.transaction("accounts", "readonly"); let transaction = db.transaction("accounts", "readonly");
const objectStore = transaction.objectStore("accounts"); let objectStore = transaction.objectStore("accounts");
const getAllRequest = objectStore.getAll(); let getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = function () { getAllRequest.onsuccess = function () {
resolve(getAllRequest.result); resolve(getAllRequest.result);
@@ -494,77 +460,74 @@ async function fetchAllAccounts() {
} }
async function getNotificationCount() { async function getNotificationCount() {
let secret = null;
let accounts = []; let accounts = [];
let result = null; let result = null;
// 1 is our master settings ID; see MASTER_SETTINGS_KEY if ("secret" in self) {
const settings = await getSettingById(1); secret = self.secret;
let lastNotifiedClaimId = null;
if ("lastNotifiedClaimId" in settings) {
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
}
const activeDid = settings["activeDid"];
accounts = await fetchAllAccounts();
let activeAccount = null;
for (let i = 0; i < accounts.length; i++) {
if (accounts[i]["did"] == activeDid) {
activeAccount = accounts[i];
break;
}
}
const headers = {
"Content-Type": "application/json",
};
const identity = activeAccount && activeAccount["identity"];
if (identity && "secret" in self) {
const secret = self.secret;
const secretUint8Array = self.decodeBase64(secret); const secretUint8Array = self.decodeBase64(secret);
const messageWithNonceAsUint8Array = self.decodeBase64(identity); // 1 is our master settings ID; see MASTER_SETTINGS_KEY
const nonce = messageWithNonceAsUint8Array.slice(0, 24); const settings = await getSettingById(1);
const message = messageWithNonceAsUint8Array.slice(24, identity.length); let lastNotifiedClaimId = null;
const decoder = new TextDecoder("utf-8"); if ("lastNotifiedClaimId" in settings) {
const decrypted = self.secretbox.open(message, nonce, secretUint8Array); lastNotifiedClaimId = settings["lastNotifiedClaimId"];
const msg = decoder.decode(decrypted); }
const identifier = JSON.parse(JSON.parse(msg)); const activeDid = settings["activeDid"];
accounts = await fetchAllAccounts();
let did = null;
for (var i = 0; i < accounts.length; i++) {
let account = accounts[i];
let did = account["did"];
if (did == activeDid) {
let publicKeyHex = account["publicKeyHex"];
let identity = account["identity"];
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
const decoder = new TextDecoder("utf-8");
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
headers["Authorization"] = "Bearer " + (await accessToken(identifier)); const msg = decoder.decode(decrypted);
} const identifier = JSON.parse(JSON.parse(msg));
const response = await fetch( const headers = {
settings["apiServer"] + "/api/v2/report/claims", "Content-Type": "application/json",
{ };
method: "GET",
headers: headers, headers["Authorization"] = "Bearer " + (await accessToken(identifier));
},
); let response = await fetch(
if (response.status == 200) { settings["apiServer"] + "/api/v2/report/claims",
const json = await response.json(); {
const claims = json["data"]; method: "GET",
let newClaims = 0; headers: headers,
for (let i = 0; i < claims.length; i++) { },
const claim = claims[i]; );
if (claim["id"] === lastNotifiedClaimId) { if (response.status == 200) {
let json = await response.json();
let claims = json["data"];
let newClaims = 0;
for (var i = 0; i < claims.length; i++) {
let claim = claims[i];
if (claim["id"] === lastNotifiedClaimId) {
break;
}
newClaims++;
}
if (newClaims > 0) {
result = `There are ${newClaims} new activities on TimeSafari`;
}
const most_recent_notified = claims[0]["id"];
await setMostRecentNotified(most_recent_notified);
} else {
console.error("The service worker got a bad response status when fetching claims:", response.status, response);
}
break; break;
} }
newClaims++;
} }
if (newClaims > 0) {
result = `There are ${newClaims} new activities on Time Safari`;
}
const most_recent_notified = claims[0]["id"];
await setMostRecentNotified(most_recent_notified);
} else {
console.error(
"safari-notifications getNotificationsCount got a bad response status when fetching claims",
response.status,
response,
);
} }
return result; return result;
} }
self.appendDailyLog = appendDailyLog;
self.getNotificationCount = getNotificationCount; self.getNotificationCount = getNotificationCount;
self.decodeBase64 = decodeBase64; self.decodeBase64 = decodeBase64;

View File

@@ -1,8 +1,4 @@
const { defineConfig } = require("@vue/cli-service"); const { defineConfig } = require("@vue/cli-service");
const { gitDescribeSync } = require("git-describe");
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true, transpileDependencies: true,
configureWebpack: { 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.) * Active. (User can change to Muted when the user mutes notifications.)
* Muted. (User can change to Active when the user toggles it.) * Muted. (User can change to Active when the user toggles it.)
(Turning mute off automatically after some amount of time is not planned in version 1.) (Turning mute off automatically after some amount of time is not planned in version 1.)
# TROUBLESHOOTING
## Desktop
#### Firefox
Go to `about:debugging` and click on `Inspect` for the service worker.
#### Chrome
Go to `chrome://inspect/#service-workers` and click on `Inspect` for the service worker.
## Mobile
#### Android
#### iOS