Compare commits
85 Commits
0.2.1
...
starred-pr
| Author | SHA1 | Date | |
|---|---|---|---|
| a1d23ff1b0 | |||
| 37690cc855 | |||
| 5f9edea116 | |||
| f517b09ed7 | |||
| ca70b19831 | |||
| f41e541fe2 | |||
| 5c547783a7 | |||
| 8d2dd6357a | |||
| 189261e991 | |||
| 15464602f9 | |||
| 331c4f64d6 | |||
| 28ae317958 | |||
| 643718619e | |||
| c3819ec919 | |||
| 719e3a467d | |||
| b251d7e4fd | |||
| 61c3a0e30b | |||
| a76df55224 | |||
| e140da081f | |||
| 1be899c48d | |||
| 6aee93ca6c | |||
| 5412625d05 | |||
| 8f579b40a9 | |||
| e8a907c63a | |||
| f53a6f3045 | |||
| b38ebc45e1 | |||
| c51d2629b3 | |||
| e642b99ff5 | |||
| 26f1e88f5a | |||
| 2e164dfeff | |||
| d7530ff56b | |||
| 2db52cb72e | |||
| c8eb3bfbc0 | |||
| 71b210d541 | |||
| 66289ec206 | |||
| 639dc7b4e5 | |||
| 4fe072f19e | |||
| f253f0af0f | |||
| 2d95a35905 | |||
| 88f869d600 | |||
| a0911bb0fd | |||
| 1053b78ab8 | |||
| dcfa8d9451 | |||
| dd38f76ee1 | |||
| 667e1e8890 | |||
| 1731f2443b | |||
| e1cffcda2d | |||
| a5b1b97012 | |||
| 563b5793a9 | |||
| 660436c8fa | |||
| 31a7752168 | |||
| 3ebe7bc156 | |||
| 0eb16d5661 | |||
| edb09da10f | |||
| be6ec6745a | |||
| b79c5fcf91 | |||
| 9dea4066c9 | |||
| 9b586566f0 | |||
| e5e702f8a5 | |||
| 32c9076c39 | |||
| 6ab4c40fd0 | |||
| d7ef07c2e2 | |||
| 9f595040d8 | |||
| 40a8794649 | |||
| fa72d38d18 | |||
| 31aacb286f | |||
| 2511f18fa7 | |||
| febfa8b098 | |||
| e0fcb1f67b | |||
| 9183092325 | |||
| a87179d127 | |||
| 14e203dd74 | |||
| acaaf8776d | |||
| cb1f38c182 | |||
| cfa7466b94 | |||
| f998364c72 | |||
| 7b4f084b4b | |||
| 115329e26c | |||
| 61bef57563 | |||
| a5368d0f82 | |||
| 48cb45d230 | |||
| 8a7ce0fe65 | |||
| 525d3fc15a | |||
| 68f3b79983 | |||
| 5353fe770a |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
signature.bin
|
signature.bin
|
||||||
|
# generated during `npm run build`
|
||||||
|
sw_scripts-combined.js
|
||||||
*.pem
|
*.pem
|
||||||
verified.txt
|
verified.txt
|
||||||
myenv
|
myenv
|
||||||
|
|||||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -5,16 +5,85 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Changed in DB
|
||||||
|
- ?
|
||||||
|
|
||||||
|
|
||||||
## [0.2.1] - 2024.01.05
|
## [0.2.14] - 2024.02.14
|
||||||
|
### Changed
|
||||||
|
- Combine all service worker scripts into a single file
|
||||||
|
### Changed in DB
|
||||||
|
- Nothing
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.13] - 2024.02.07
|
||||||
|
### Added
|
||||||
|
- Display of user's offers
|
||||||
|
- Check for valid DIDs
|
||||||
|
### Fixed
|
||||||
|
- Name display on give prompt
|
||||||
|
- Non-numbers on number input & autocapitalize on URL input
|
||||||
|
### Changed in DB
|
||||||
|
- Nothing
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.12] - 2024.02.01
|
||||||
|
### Added
|
||||||
|
- Prompts for gratitude
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.11] - 2024.01.28
|
||||||
|
### Added
|
||||||
|
- Actions to share claim data with contacts
|
||||||
|
- Bulk CSV import from Endorser Mobile export
|
||||||
|
- Dates on give summaries
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.10] - 2024.01.18 - 667e1e8890b42de59cd939caca1a01c7a7a702be
|
||||||
|
### Added
|
||||||
|
- Person identicons for contacts
|
||||||
|
- Confirmation & delivery directly from project page
|
||||||
|
- Offer dialog now allows units
|
||||||
|
- Links from claim detail page to the fulfilled project or offer
|
||||||
|
- Link to project from home feed
|
||||||
|
- Copy to clipboard in more places
|
||||||
|
### Fixed
|
||||||
|
- "More Contacts" for give on project page now links correctly.
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.9] - 2024.01.15 - e5e702f8a5a53a6efbed48d35f0bc3cee63024a0
|
||||||
|
### Fixed
|
||||||
|
- Set visibility for new contact.
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.8] - 2024.01.14
|
||||||
|
### Added
|
||||||
|
- Automatic ID creation from home page
|
||||||
|
- Agent who can also edit a project
|
||||||
|
### Fixed
|
||||||
|
- Cannot declare anonymous gift
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.7] - 2024.01.12
|
||||||
|
### Added
|
||||||
|
- Give to fulfill a particular offer
|
||||||
|
- Give as part of a trade as opposed to a donation
|
||||||
|
- Error notifications on import
|
||||||
|
### Changed
|
||||||
|
- Library security updates
|
||||||
|
- Visibility of actions & confirmations on claim page
|
||||||
|
### Fixed
|
||||||
|
- Name of offerer
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.2] - 2024.01.05
|
||||||
### Added
|
### Added
|
||||||
- Check for notification capability on front screen
|
- Check for notification capability on front screen
|
||||||
- Contact next-public-key-hash in manual textual input
|
- Contact next-public-key-hash in manual textual input
|
||||||
- Confirmation for contact visibility change
|
- Confirmation for contact visibility change
|
||||||
- YAML rendering of full claim details
|
- YAML rendering of full claim details
|
||||||
|
- Hints for onboarding on the contact screen
|
||||||
|
|
||||||
|
|
||||||
## [0.2.0] - 2024.01.04
|
## [0.2.0] - 2024.01.04
|
||||||
@@ -23,6 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Icon for Android
|
- Icon for Android
|
||||||
- More thorough messaging and testing for notifications
|
- More thorough messaging and testing for notifications
|
||||||
|
|
||||||
|
|
||||||
## [0.1.9] - 2024.01.01
|
## [0.1.9] - 2024.01.01
|
||||||
### Added
|
### Added
|
||||||
- Import for contacts and settings
|
- Import for contacts and settings
|
||||||
|
|||||||
6
CONTRIBUTING.md
Normal file
6
CONTRIBUTING.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Welcome! We are happy to have your help with this project.
|
||||||
|
|
||||||
|
Note that all contributions will be under our
|
||||||
|
[license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE).
|
||||||
117
README.md
117
README.md
@@ -1,6 +1,14 @@
|
|||||||
# TimeSafari.app - Crowd-Funder for Time - PWA
|
# TimeSafari.app - Crowd-Funder for Time - PWA
|
||||||
|
|
||||||
## Project setup
|
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
|
||||||
|
and expand to crowd-fund with time & money, then record and see the impact of contributions.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
See [project.task.yaml](project.task.yaml) for current priorities.
|
||||||
|
(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
We have pkgx.dev set up in package.json, so you can use `dev` to set up the dev environment.
|
We have pkgx.dev set up in package.json, so you can use `dev` to set up the dev environment.
|
||||||
|
|
||||||
@@ -20,27 +28,25 @@ npm run lint
|
|||||||
|
|
||||||
### Compiles and minifies for production
|
### Compiles and minifies for production
|
||||||
|
|
||||||
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
|
||||||
|
|
||||||
|
* `npx prettier --write ./sw_scripts/`
|
||||||
|
|
||||||
* Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit.
|
* Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit.
|
||||||
|
|
||||||
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
|
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
|
||||||
|
|
||||||
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test". Also record what version is on production.
|
... though maybe you do that after testing and release, since that isn't used in the build (and you often increment a lot during testing).
|
||||||
|
|
||||||
|
* If production: change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test". Also record what version is on production.
|
||||||
|
|
||||||
* `npm run build`
|
* `npm run build`
|
||||||
|
|
||||||
* `npx prettier --write ./sw_scripts/`
|
* Get on the server and back up the time-safari folder.
|
||||||
|
|
||||||
...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 ubuntutest@test.timesafari.app:time-safari`
|
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
|
||||||
|
|
||||||
* Revert src/constants/app.ts and package.json (if that was prod), edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
* Revert src/constants/app.ts and package.json (if that was prod), edit package.json to increment version & add "-beta", `npm install`, and commit. Tag if you didn't before. Also record what version is on production.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -49,17 +55,10 @@ If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js,
|
|||||||
### Register new user on test server
|
### Register new user on test server
|
||||||
|
|
||||||
On the test server, User #0 has rights to register others, so you can start
|
On the test server, User #0 has rights to register others, so you can start
|
||||||
playing one of two ways:
|
playing by importing that user and registering others. Import the keys for the test User
|
||||||
|
`did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F` by importing this seed phrase:
|
||||||
- Import the keys for the test User `did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F` by importing this seed phrase:
|
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
||||||
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
||||||
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
|
||||||
|
|
||||||
- Alternatively, register someone else under User #0 automatically:
|
|
||||||
|
|
||||||
* In the `src/views/AccountViewView.vue` file, uncomment the lines referring to "testServerRegisterUser".
|
|
||||||
|
|
||||||
* Visit the `/account` page.
|
|
||||||
|
|
||||||
### Create multiple identifiers
|
### Create multiple identifiers
|
||||||
|
|
||||||
@@ -77,38 +76,39 @@ For your own web-push tests, change the push server URL in Advanced settings on
|
|||||||
|
|
||||||
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
|
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
|
||||||
|
|
||||||
### Manual walk-through
|
### Manual walk-through test
|
||||||
|
|
||||||
- Clear the browser cache for localhost for a new user.
|
- If there were any DB changes, check that you're on the old version and reload the page and ensure you can still act.
|
||||||
- See that it's using the test API.
|
- Use a mobile user as well as a desktop user.
|
||||||
- On each page, verify the messaging.
|
- Backup seed & data & get a CSV dump from Endorser Mobile.
|
||||||
- On the home page, see the feed without names, and see a message prompting to generate an ID.
|
- Check that the version is updated.
|
||||||
|
- Clear the browser data & add identity & import Time Safari contacts and then CSV contacts.
|
||||||
|
- Make sure that it's using the test API (under Identity in 'Advanced').
|
||||||
|
- Clear the browser data again. (See "Reset" below.)
|
||||||
|
- Go to the account page before visiting the home page to see that there is no ID.
|
||||||
|
- On the home page:
|
||||||
|
- Check that it generated an ID.
|
||||||
|
- Check the feed without names.
|
||||||
|
- Copy the contact URL.
|
||||||
|
- On each page, verify the messaging, and that they cannot take action.
|
||||||
- On the discovery page, check that they can see projects, and set a search area to see projects nearby.
|
- On the discovery page, check that they can see projects, and set a search area to see projects nearby.
|
||||||
- As User #0 in another browser on the test API, add a give & a project. (See User #0 details above.)
|
- On the contacts page, check that they can add User #0 even without their own ID.
|
||||||
|
- As User #0 in another browser on the test API, add a give & a project.
|
||||||
|
- `rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
||||||
- With the new user on the home page, see the feed that shows User #0 in network but without the name.
|
- With the new user on the home page, see the feed that shows User #0 in network but without the name.
|
||||||
- As the new user on the contacts page, add User #0 as a contact.
|
- As the new user on the contacts page, add User #0 as a contact.
|
||||||
- On the home page, see the feed that shows User #0 with a name.
|
- On the home page, see the feed that shows User #0 with a name.
|
||||||
- Generate an ID.
|
- Switch back to the generated identifier.
|
||||||
- On the home page, check that it now prompts them to get registered.
|
|
||||||
- On the account page, check that they see messages on limits.
|
- On the account page, check that they see messages on limits.
|
||||||
- Register the ID from User #0.
|
- As User #0, register the ID.
|
||||||
- As the new user on the home page, check that they can now record a gift.
|
- As the new user on the home page, check that they can now record a gift, and record an offer & delivery.
|
||||||
- On the contacts page, check that they cannot register someone else yet.
|
- On the contacts page, check that they cannot register someone else yet.
|
||||||
- Walk through the functions on each page.
|
- Walk through the functions on each page.
|
||||||
|
- Set and run notifications.
|
||||||
|
- Export & import, both seed and contacts & settings.
|
||||||
|
- Choose location on the search map.
|
||||||
## Scenarios
|
- Offer, deliver a give, and confirm. Create a third user and test connections.
|
||||||
|
- Switch to "no identifier" to see that things look OK without any ID.
|
||||||
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
|
|
||||||
|
|
||||||
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
|
|
||||||
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
|
||||||
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
|
||||||
|
|
||||||
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
|
|
||||||
|
|
||||||
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
|
|
||||||
|
|
||||||
### Clear/Reset data & restart
|
### Clear/Reset data & restart
|
||||||
|
|
||||||
@@ -121,6 +121,22 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
* A problem with `GET http://localhost:8080/web-push/vapid` means the py-push-server is not running
|
||||||
|
(and notifications won't work for a local app without special routing from the browser's web push service provider, anyway).
|
||||||
|
|
||||||
|
* Red errors everywhere with a console message like this:
|
||||||
|
`Error: An ID is chosen but there are no keys for it so it cannot be used to talk with the service`
|
||||||
|
... has happened on account switching when the current account was erased (or maybe replaced -- once I had a duplicate and I don't know how).
|
||||||
|
|
||||||
|
* The error `DEXIE ENCRYPT ADDON: Could not decrypt message!` or
|
||||||
|
`Encryption key has changed` means that the encryption key is wrong,
|
||||||
|
sometimes seen after clearing storage for testing; you can make it happen by clearing localStorage.
|
||||||
|
Maybe only part of the storage was cleared out. Unless you got a copy of that password, you'll
|
||||||
|
have to erase storage and reload the identifier.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
@@ -131,13 +147,18 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
|
|||||||
|
|
||||||
* [Customize Vue configuration](https://cli.vuejs.org/config/).
|
* [Customize Vue configuration](https://cli.vuejs.org/config/).
|
||||||
|
|
||||||
|
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||||
|
|
||||||
|
|
||||||
### Kudos
|
### Kudos
|
||||||
|
|
||||||
Gifts make the world go 'round!
|
Gifts make the world go 'round!
|
||||||
|
|
||||||
|
* [WebStorm by JetBrains](https://www.jetbrains.com/webstorm/) for the free open-source license
|
||||||
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
|
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
|
||||||
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
* [Many tools & libraries](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/branch/master/package.json#L10) 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)
|
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
|
||||||
|
* [DiceBear](https://www.dicebear.com/licenses/) and [Avataaars](https://www.dicebear.com/styles/avataaars/#details) for human-looking identicons
|
||||||
|
* Some gratitude prompts thanks to [Develop Good Habits](https://www.developgoodhabits.com/gratitude-journal-prompts/)
|
||||||
|
|||||||
424
package-lock.json
generated
424
package-lock.json
generated
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari_Test",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.2.1",
|
"version": "0.2.15-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "TimeSafari_Test",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.2.1",
|
"version": "0.2.15-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dicebear/collection": "^5.3.5",
|
||||||
|
"@dicebear/core": "^5.3.5",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
@@ -40,7 +42,7 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"localstorage-slim": "^2.5.0",
|
"localstorage-slim": "^2.5.0",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.4.3",
|
||||||
"merkletreejs": "^0.3.10",
|
"merkletreejs": "^0.3.11",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
@@ -2423,6 +2425,358 @@
|
|||||||
"node": ">=8.9"
|
"node": ">=8.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dicebear/adventurer": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-nbW5xOQ6Y/Ca4gD5bjMUCAsvRR8QswmWIGHEEq+dGOWsKMWI1xiS/ANUlaPjkEl78DeKfPFaBPUdsZCTnQHjvA==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/adventurer-neutral": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-IRui1HtGNSw2THVSrArZGHKBeg9sE+QDax4VmJpFyKGxUEqbjnb0GizvNQEeKYt4uu9OGFYXqQm8uAzx+uMk8w==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/avataaars": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-z9pEauaqRJGbABghFJAuhy4NSS9tHuNpmVdvSarVMn0b6fKxSxqpH6Om2lENXwEPze1EoOdvm5jPnDk3/EAFYA==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/avataaars-neutral": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-AiFhb0GwR1ouDX3ooIDufnSRkjckmqiUSmmGR3sa63qNyzrhgNZDRY9l5eixXn00eOW3FW7cRu8L72+dTBXeVA==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/big-ears": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-2qy6M8pToQdzDzgVXN+9g70y4QioMZRF0qM5p1a5UiSwMZl8p1sI6rTM8WYKWVsrMTaZkeLiDCvq0N84YhLmug==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/big-ears-neutral": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-9xqno7IzMDGVfv8zaH0s4vcRSH//LIW6Vaee9TiJzqb7xo2fUaSJcnZdwh4HQ72sJzPfxb9wwhqRZJRyczU1KA==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/big-smile": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-fXO50cB1WjCKRpv9cKhFroxwy8HzuUaM8iB2lzPYuOG3MJ838GnYGC26REXdmTgu0Xo9AGSQLh4AlpmXYzOgRg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/bottts": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-rk6kmCy4AGMIJ4FuA90PvQoOlejAoYqmXyCJRDfiVuzWBZZ36bMqwY/MHGNjXncsC0EMiPPqPFSKRNUXEy+X2A==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/bottts-neutral": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-yichPU4ijqkYEsD6O9TMnmE2iOJ401/HAclpeixBFktEIEF7khUZ3Pmg08qMKPDGZMX5Syl9jm4KDAT9gDp9eg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/collection": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-zWZjvBRaAY6smU5ynqpeFFufyGCnrAIwND1v/SjW1tyDzRY4DZ34opxibQYqL4bODPFsiTsFb8M7Jejkg324Yg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@dicebear/adventurer": "5.3.5",
|
||||||
|
"@dicebear/adventurer-neutral": "5.3.5",
|
||||||
|
"@dicebear/avataaars": "5.3.5",
|
||||||
|
"@dicebear/avataaars-neutral": "5.3.5",
|
||||||
|
"@dicebear/big-ears": "5.3.5",
|
||||||
|
"@dicebear/big-ears-neutral": "5.3.5",
|
||||||
|
"@dicebear/big-smile": "5.3.5",
|
||||||
|
"@dicebear/bottts": "5.3.5",
|
||||||
|
"@dicebear/bottts-neutral": "5.3.5",
|
||||||
|
"@dicebear/croodles": "5.3.5",
|
||||||
|
"@dicebear/croodles-neutral": "5.3.5",
|
||||||
|
"@dicebear/fun-emoji": "5.3.5",
|
||||||
|
"@dicebear/icons": "5.3.5",
|
||||||
|
"@dicebear/identicon": "5.3.5",
|
||||||
|
"@dicebear/initials": "5.3.5",
|
||||||
|
"@dicebear/lorelei": "5.3.5",
|
||||||
|
"@dicebear/lorelei-neutral": "5.3.5",
|
||||||
|
"@dicebear/micah": "5.3.5",
|
||||||
|
"@dicebear/miniavs": "5.3.5",
|
||||||
|
"@dicebear/open-peeps": "5.3.5",
|
||||||
|
"@dicebear/personas": "5.3.5",
|
||||||
|
"@dicebear/pixel-art": "5.3.5",
|
||||||
|
"@dicebear/pixel-art-neutral": "5.3.5",
|
||||||
|
"@dicebear/shapes": "5.3.5",
|
||||||
|
"@dicebear/thumbs": "5.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/converter": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/converter/-/converter-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-o/HEBQ4Jr8wTqpGYeQ7T9tmQjOiwVsi6PFwxIW8+99WL1RYfWNFZdrqtDs0CdfRt+C5xcD1lVVOp7AfNfZYWnA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/json-schema": "^7.0.7",
|
||||||
|
"tmp-promise": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@resvg/resvg-js": "^2.0.0",
|
||||||
|
"exiftool-vendored": "^16 || ^17 || ^18 || ^19 || ^20 || ^21",
|
||||||
|
"sharp": "^0.32.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@resvg/resvg-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"exiftool-vendored": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sharp": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/core": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/core/-/core-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-kQPH3LGFUwqkfetUvfNZyY8l7RgomESPTkVjxpQS0n2i6KzjFe7fpDGedVAkZwGOqOHSSaq5doENCTV1uDSC8Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@dicebear/converter": "5.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/croodles": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-TywdSL8ztt9rdIypMFDdBVEVHzaKL4044PPQtkrOH/elWkOrP+tvoccxGSouembRHeOHqCYXTafwItf6UMD6+Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/croodles-neutral": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-l2Pw6k5UVajUZJOCy/0VTTUIhJcF+JVVwTy0XTKki3VCEeObhT8TAL67uRPFUZe6Xc8rC+DBBRXolglW4HrQ+g==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/fun-emoji": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-20hVdZPWAT+Bw/SJeeApMuhd5N8wNkeMMdd2jZaQG9/cd4F4NtWfXZcGRNoiz76SvrgTr1fFxi13mKu6osNPZA==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/icons": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-tFHQOnnWzQSsSeNro2Ld3Mo7a+ngl03nY4YWwbpg/sn48L7n1YdDkFRrQofl9Wv8jqCsqEOv+SL9y81e7eDBBg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/identicon": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-MMSwOsKVp9Y72P7Sv1Wr3cj05nmlHnCFLiWgxVmXBHWzbx2qIb5WjAeACoU6d6XF1NJo1M4ICJ9VP+V8vq+tOg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/initials": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-SJv1pAzhQy15CzJqafxGtEbhxhWtE2/ai3udpHFYVBGZk3l3By7iau1AyDdB0GjZkg8XyE98ThH+3625OBKzpQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/lorelei": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-wApyFlSrA7FIcHC9MSFEumtCKFhXXWLnmDmNUW7gSmrUkleNDmbZxImBKIXCyl7KIN7Ckos13oPB4lag5r5DeA==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/lorelei-neutral": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-IE8RhLBVWSGEswm57ciAZ5YKcwo4rm3KIjdXevEiyYEm6BTPA2dzSB1JDGsqXGTpBe6kt9GRd5CibuBfD96p0w==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/micah": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-cfEyFYrAPcmsZs2UoGuy9cEuwRgBBjV8TBxB+RQ/nS0ENeQ5pZ4qsgWQbFJUqtHfm2CAvMkkuQ2DfLo29SmPjg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/miniavs": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-Uj8YSA0RY1jxJ0Ki6Yq4ERUeIsuFazKQbqfpAZLJBiUELq7WnASyaPqDjmAz5l2+dbVhpHSsQIzkLeejT6a/0Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/open-peeps": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-Od/c2hjK+VJWb4PXS9G3ln50Kjl30o8Ns3+mhkoEgsKf82Dbiyp2JtKzZoizZxAqcPK93R+qrjDYumq22S9f4w==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/personas": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-auLmS+y3Bhok+SkwLCDgTF0lglDaiNv6BrQpYS0/jvUMXRmFlqmOiA+dESP5Qa1jzdjrl+D9fxlRoS24qasIyg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/pixel-art": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-9i8przKdtzipYa4Bf2dHYgPIogcHEhctAKxi6BBOzcZKbzXjYc6An3E2E1JLNcDeGGbAE4XKt8INvMOYCHxv9A==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/pixel-art-neutral": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-aBtEaJeEa/YOy5aTTXmH2S/9Q8bcjIWEsXNN0MMoksISJyfrqcZj1Nm1/NeSvXvNWkTWj0kVFi2TOMr804Hj3Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/shapes": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-aGN+p0D1Xuwg+OwoTHAqLMYLIZRZe0YpcIH8FwfgEFK/4Yo+NqqAebmnRwh3yHejCknURteq853YhDhIbvJMaw==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dicebear/thumbs": {
|
||||||
|
"version": "5.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-5.3.5.tgz",
|
||||||
|
"integrity": "sha512-uSsTOCZKiUWYpQG/jNTYTtF/h1vzPFrvtq2gUbhec4mKgdCMCsOnk0JMYUKte03Oly/a2XrtJjPL7/TlxqCXXA==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dicebear/core": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@digitalbazaar/bitstring": {
|
"node_modules/@digitalbazaar/bitstring": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@digitalbazaar/bitstring/-/bitstring-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@digitalbazaar/bitstring/-/bitstring-3.1.0.tgz",
|
||||||
@@ -8805,8 +9159,7 @@
|
|||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.13",
|
"version": "7.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
||||||
"integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==",
|
"integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/leaflet": {
|
"node_modules/@types/leaflet": {
|
||||||
"version": "1.9.6",
|
"version": "1.9.6",
|
||||||
@@ -10453,9 +10806,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@vue/vue-loader-v15": {
|
"node_modules/@vue/vue-loader-v15": {
|
||||||
"name": "vue-loader",
|
"name": "vue-loader",
|
||||||
"version": "15.10.2",
|
"version": "15.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.11.1.tgz",
|
||||||
"integrity": "sha512-ndeSe/8KQc/nlA7TJ+OBhv2qalmj1s+uBs7yHDRFaAXscFTApBzY9F1jES3bautmgWjDlDct0fw8rPuySDLwxw==",
|
"integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/component-compiler-utils": "^3.1.0",
|
"@vue/component-compiler-utils": "^3.1.0",
|
||||||
@@ -11274,11 +11627,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.5.1",
|
"version": "1.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
|
||||||
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
|
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.0",
|
"follow-redirects": "^1.15.4",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
@@ -11825,7 +12178,6 @@
|
|||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -12657,8 +13009,7 @@
|
|||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/connect": {
|
"node_modules/connect": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
@@ -12954,9 +13305,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/crypto-js": {
|
"node_modules/crypto-js": {
|
||||||
"version": "3.3.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
"integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q=="
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
},
|
},
|
||||||
"node_modules/crypto-ld": {
|
"node_modules/crypto-ld": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
@@ -15882,9 +16233,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.3",
|
"version": "1.15.4",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||||
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -16314,7 +16665,6 @@
|
|||||||
"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",
|
||||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fs.realpath": "^1.0.0",
|
"fs.realpath": "^1.0.0",
|
||||||
"inflight": "^1.0.4",
|
"inflight": "^1.0.4",
|
||||||
@@ -20070,13 +20420,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/merkletreejs": {
|
"node_modules/merkletreejs": {
|
||||||
"version": "0.3.10",
|
"version": "0.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.3.11.tgz",
|
||||||
"integrity": "sha512-lin42tKfRdkW+6iE5pjtQ9BnH+1Hk3sJ5Fn9hUUSjcXRcJbSISHgPCfYvMNEXiNqZPhz/TyRPEV30qgnujsQ7A==",
|
"integrity": "sha512-LJKTl4iVNTndhL+3Uz/tfkjD0klIWsHlUzgtuNnNrsf7bAlXR30m+xYB7lHr5Z/l6e/yAIsr26Dabx6Buo4VGQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bignumber.js": "^9.0.1",
|
"bignumber.js": "^9.0.1",
|
||||||
"buffer-reverse": "^1.0.1",
|
"buffer-reverse": "^1.0.1",
|
||||||
"crypto-js": "^3.1.9-1",
|
"crypto-js": "^4.2.0",
|
||||||
"treeify": "^1.1.0",
|
"treeify": "^1.1.0",
|
||||||
"web3-utils": "^1.3.4"
|
"web3-utils": "^1.3.4"
|
||||||
},
|
},
|
||||||
@@ -21144,7 +21494,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
},
|
},
|
||||||
@@ -22186,7 +22535,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
"devOptional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -24411,7 +24759,6 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.1.3"
|
"glob": "^7.1.3"
|
||||||
},
|
},
|
||||||
@@ -26219,6 +26566,25 @@
|
|||||||
"node": ">=0.6.0"
|
"node": ">=0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tmp-promise": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tmp": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tmp-promise/node_modules/tmp": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"rimraf": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tmpl": {
|
"node_modules/tmpl": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari_Test",
|
"name": "TimeSafari_Test",
|
||||||
"version": "0.2.1",
|
"version": "0.2.15-beta",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dicebear/collection": "^5.3.5",
|
||||||
|
"@dicebear/core": "^5.3.5",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
@@ -40,7 +42,7 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"localstorage-slim": "^2.5.0",
|
"localstorage-slim": "^2.5.0",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.4.3",
|
||||||
"merkletreejs": "^0.3.10",
|
"merkletreejs": "^0.3.11",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
|
|||||||
@@ -1,32 +1,89 @@
|
|||||||
|
|
||||||
tasks:
|
tasks :
|
||||||
|
|
||||||
- show VC details... somehow:
|
- 01 release server & client for fix to project-editing
|
||||||
- 01 show my VCs - most interesting, or via search
|
|
||||||
- 04 allow user to download chains of VCs, mine + ones I can see about me from others
|
- .5 fix timeSafari.org cert renewals
|
||||||
- add VC confirmation
|
- .2 anchor hash into BTC
|
||||||
|
- .1 add step 1 to onboarding hints to "install"
|
||||||
|
|
||||||
|
- 01 bookmarks for BVC
|
||||||
|
|
||||||
|
- 24 compelling UI for credential presentations
|
||||||
|
- discover who in my network has activity on a project
|
||||||
|
- 24 compelling UI for statistics (eg. World?)
|
||||||
|
|
||||||
|
- .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window)
|
||||||
|
- .2 add links between projects
|
||||||
|
- 32 image on give :
|
||||||
|
- Show a camera to take a picture
|
||||||
|
- Scale the image to a reasonable size
|
||||||
|
- Upload to a public readable place
|
||||||
|
- check the rate limits
|
||||||
|
- use CID
|
||||||
|
- put the image URL in the claim
|
||||||
|
- Rates - images erased?
|
||||||
|
- image not associated with JWT ULID since that's assigned later
|
||||||
|
|
||||||
|
- 24 make the contact browsing on the front page something that invites more action
|
||||||
|
|
||||||
|
- .2 list the "show more" contacts alphabetically
|
||||||
|
- .5 change server plan & project endpoints to use jwtId as identifier rather than rowid
|
||||||
|
- 16 edit offers & gives, or revoke allowing re-creation
|
||||||
|
- .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page.
|
||||||
|
- .1 Add help instructions for "Encryption key has changed" error. (It is a problem if localStorage is cleared, but the contacts & settings remain and they have to restore their seeds.)
|
||||||
|
- .5 add more detail on TimeSafari.org
|
||||||
|
- .1 show better error when user with no ID goes to the "My Project" page
|
||||||
|
- 08 add button to front page to prompt for ideas for gratitude :
|
||||||
|
- show previous on "Your" screen
|
||||||
|
- checkboxes - randomize vs show in order, show non-person-oriented messages, show only contacts, show only projects
|
||||||
|
|
||||||
|
- 08 allow user to add a time when they want their daily notification
|
||||||
|
|
||||||
|
- .5 prompt for the name directly when they visit the QR scan page
|
||||||
|
- 01 mark a project as inactive
|
||||||
|
- 01 add share button for sending a message to confirmers when we can't see the claim (like the "visible" links)
|
||||||
|
- .5 add TimeSafari as a shareable app https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target
|
||||||
|
- .5 choose a project's alternative agent ("authorized representative") via a contact chooser (not just copy-paste a DID)
|
||||||
|
- .5 find out why clicking quickly back-and-forth onto the "my project" page often shows error "You need an identifier to load your projects." (easier to reproduce on desktop?)
|
||||||
|
- .5 bug - it didn't show the "fulfills offer" on the claim detail page for a give that had one - https://test.timesafari.app/claim/01HMFWRPA3PD6Q9EYFKX3MC41J
|
||||||
|
- 01 replace all "confirm" prompts with nicer modal
|
||||||
|
- .1 hide project-create button on project page if not registered
|
||||||
|
- .1 hide offer & give buttons on project list page if not registered
|
||||||
|
- .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page
|
||||||
|
- .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads
|
||||||
|
|
||||||
|
- bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.)
|
||||||
|
- refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute)
|
||||||
|
- the confirm button on each give on the ProjectViewView page doesn't have all the context of the ClaimView page, so it can show sometimes inappropriately; consider consolidation
|
||||||
|
- make the "give" on contact screen work like other give (allowing donation vs current blank)
|
||||||
|
- .2 on ClaimView, the "ask someone" should refer to "visible" IDs, or to confirmations only if confirmations are visible
|
||||||
|
- message "send them to this page" on ClaimView should be a link (for installed app)
|
||||||
|
- When we update a version, desktop browser users have seen nothing happen after clicking on the contact page QR and on the account page "Help"; errors show in the console. Reload fixed it. If this happens on mobile, ask the user to reload.
|
||||||
|
|
||||||
|
- 01 show my VCs - most interesting, or via search
|
||||||
|
- 04 allow user to download & prove chains of VCs, mine + ones I can see about me from others
|
||||||
|
|
||||||
|
- show feed of offers, new projects, etc -- maybe limited to my search area
|
||||||
|
|
||||||
|
- revenue to support server operation
|
||||||
|
|
||||||
- look for "if is a new user" -- blank name
|
|
||||||
- copy button for seed
|
- copy button for seed
|
||||||
|
|
||||||
- .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
|
||||||
- record donations vs gives
|
|
||||||
- make server endpoint for full English description of limits
|
- make server endpoint for full English description of limits
|
||||||
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
|
- create a help-desk document & add screenshots
|
||||||
|
|
||||||
- 01 server - show all claim details when issued by the issuer
|
- .1 update "offer" units to have same functionality as "give" units
|
||||||
- bug - got error adding on Firefox user #0 as contact for themselves
|
- 01 on home page, prompt for install check in addition to "supports notifications" check (since they won't get notified if Chrome is closed)
|
||||||
- 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
|
- 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list)
|
||||||
|
- bug (that is hard to reproduce) - got error adding on Firefox user #0 as contact for themselves
|
||||||
- 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) - 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
|
- 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 remove 'rowid' references (that are sqlite-specific); may involve server
|
||||||
- 04 look at other examples for better UI friend.tech
|
- 04 look at other examples for better onboarding UI, eg friend.tech
|
||||||
- 01 make the prod build copy the sw_scripts
|
- .5 Add inactive flag / end date, 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?
|
||||||
- .5 customize favicon assignee-group:ui
|
|
||||||
- .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
|
||||||
@@ -37,8 +94,9 @@ tasks:
|
|||||||
- warn if they're using the web (android only?)
|
- warn if they're using the web (android only?)
|
||||||
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
|
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
|
||||||
https://web.dev/articles/get-installed-related-apps
|
https://web.dev/articles/get-installed-related-apps
|
||||||
|
- .5 fix the "onboarding help" list of instructions so that it always formats right (currently doesn't show numbers aligned on Google Pixel 6a, iPhone 11 Pro, iPhone 12 mini)
|
||||||
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
|
- .5 make the "onboarding help" it so that it doesn't cover the QR icon on the contacts page
|
||||||
|
- .5 fix masked icon (because some of the top-right of the binoculars is cut off)
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
@@ -71,7 +129,6 @@ tasks:
|
|||||||
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
||||||
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
|
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
|
||||||
|
|
||||||
- .5 add "back" button to all screens that aren't part of the bottom tray
|
|
||||||
- .5 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
|
- .5 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
|
||||||
- .5 Replace Gifted/Give in ContactsView with GiftedDialog
|
- .5 Replace Gifted/Give in ContactsView with GiftedDialog
|
||||||
|
|
||||||
@@ -83,29 +140,26 @@ tasks:
|
|||||||
- badge for amount given/offered to your project
|
- badge for amount given/offered to your project
|
||||||
- set a goal of given/offers
|
- set a goal of given/offers
|
||||||
|
|
||||||
- automated tests, eg. cypress
|
- automated tests, eg. pup-test or cypress
|
||||||
|
|
||||||
- Notifications (wake on the phone, push notifications)
|
- Notifications (wake on the phone, push notifications)
|
||||||
- 02 change push server so that the web-push/subscribe call sets up a thread for the 10-seconds-later push notification, but returns immediately to the callee
|
- 02 change push server so that the web-push/subscribe call sets up a thread for the 10-seconds-later push notification, but returns immediately to the callee
|
||||||
- pull instead of push, maybe via scheduled runs
|
- pull instead of push, maybe via scheduled runs
|
||||||
- have a notification pop-up on Mac screen
|
- have a notification pop-up on Mac screen
|
||||||
|
|
||||||
- Connect with phone contacts
|
- 16 Connect with phone contacts - this may be a whole different app, because we want a quick link A) to the same phone contact and B) from the phone contact app
|
||||||
|
|
||||||
- Multiple identities
|
|
||||||
|
|
||||||
- Support KERI AIDs
|
- Support KERI AIDs
|
||||||
- Support Peer DIDs
|
- Support Peer DIDs
|
||||||
- Support messaging through DIDComm
|
- Support messaging through DIDComm
|
||||||
- Write to or read from a different ledger (eg. private ACDC, EAS & attest.sh)
|
- Write to or read from a different ledger (eg. private ACDC, EAS & attest.sh)
|
||||||
|
|
||||||
- Do we want split first name & last name?
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
- 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
|
- 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 -- and that allows spam
|
||||||
|
- .1 When Chrome shows compatibility https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
|
||||||
|
then change the canShare check in this app to check the real canShare() method.
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ export default class App extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// there may be a long pause here on first initialization
|
// there may be a long pause here on first initialization
|
||||||
navigator.serviceWorker.ready.then(() => {
|
navigator.serviceWorker?.ready.then(() => {
|
||||||
this.serviceWorkerReady = true;
|
this.serviceWorkerReady = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -443,7 +443,7 @@ export default class App extends Vue {
|
|||||||
this.subscribeToPush()
|
this.subscribeToPush()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("Subscribed successfully.");
|
console.log("Subscribed successfully.");
|
||||||
return navigator.serviceWorker.ready;
|
return navigator.serviceWorker?.ready;
|
||||||
})
|
})
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
return registration.pushManager.getSubscription();
|
return registration.pushManager.getSubscription();
|
||||||
@@ -575,7 +575,7 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
async turnOffNotifications() {
|
async turnOffNotifications() {
|
||||||
let subscription;
|
let subscription;
|
||||||
const pushProviderSuccess = await navigator.serviceWorker.ready
|
const pushProviderSuccess = await navigator.serviceWorker?.ready
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
return registration.pushManager.getSubscription();
|
return registration.pushManager.getSubscription();
|
||||||
})
|
})
|
||||||
|
|||||||
3
src/assets/blank-square.svg
Normal file
3
src/assets/blank-square.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<rect width="64" height="64" fill="#ffffff"></rect>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 145 B |
@@ -1,30 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-html="generateIdenticon()" class="w-fit"></div>
|
<div v-html="generateIcon()" class="w-fit"></div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createAvatar, StyleOptions } from "@dicebear/core";
|
||||||
|
import { avataaars } from "@dicebear/collection";
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
import { toSvg } from "jdenticon";
|
|
||||||
|
|
||||||
const BLANK_CONFIG = {
|
|
||||||
lightness: {
|
|
||||||
color: [1.0, 1.0],
|
|
||||||
grayscale: [1.0, 1.0],
|
|
||||||
},
|
|
||||||
saturation: {
|
|
||||||
color: 0.0,
|
|
||||||
grayscale: 0.0,
|
|
||||||
},
|
|
||||||
backColor: "#0000",
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class EntityIcon extends Vue {
|
export default class EntityIcon extends Vue {
|
||||||
@Prop entityId = "";
|
@Prop entityId = "";
|
||||||
@Prop iconSize = 0;
|
@Prop iconSize = 0;
|
||||||
|
|
||||||
generateIdenticon() {
|
generateIcon() {
|
||||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
const options: StyleOptions<object> = {
|
||||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
seed: this.entityId || "",
|
||||||
|
size: this.iconSize,
|
||||||
|
};
|
||||||
|
const avatar = createAvatar(avataaars, options);
|
||||||
|
const svgString = avatar.toString();
|
||||||
return svgString;
|
return svgString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
/>
|
/>
|
||||||
<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 text-blue-500 px-2 py-2"
|
||||||
@click="changeUnitCode()"
|
@click="changeUnitCode()"
|
||||||
>
|
>
|
||||||
{{ UNIT_SHORT[unitCode] }}
|
{{ libsUtil.UNIT_SHORT[unitCode] }}
|
||||||
</span>
|
</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"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<fa icon="chevron-left" />
|
<fa icon="chevron-left" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="number"
|
||||||
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="amountInput"
|
||||||
/>
|
/>
|
||||||
@@ -36,9 +36,15 @@
|
|||||||
<fa icon="chevron-right" />
|
<fa icon="chevron-right" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showGivenToUser" class="mt-2 text-right">
|
<div class="mt-2 text-right">
|
||||||
<input type="checkbox" class="mr-2" v-model="givenToUser" />
|
<span v-if="showGivenToUser" class="mr-16">
|
||||||
<label class="text-sm">Given to you</label>
|
<input type="checkbox" class="mr-2" v-model="givenToUser" />
|
||||||
|
<label class="text-sm">Given to you</label>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<input type="checkbox" class="mr-2" v-model="isTrade" />
|
||||||
|
<label class="text-sm">Trade (not a gift)</label>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center mb-2 mt-6 italic">
|
<p class="text-center mb-2 mt-6 italic">
|
||||||
Sign & Send to publish to the world
|
Sign & Send to publish to the world
|
||||||
@@ -61,10 +67,16 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
import { createAndSubmitGive, GiverInputInfo } from "@/libs/endorserServer";
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
|
didInfo,
|
||||||
|
GiverInputInfo,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
|
import * as libsUtil from "@/libs/util";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -82,42 +94,52 @@ export default class GiftedDialog extends Vue {
|
|||||||
@Prop showGivenToUser = false;
|
@Prop showGivenToUser = false;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
|
||||||
amountInput = "0";
|
amountInput = "0";
|
||||||
giver?: GiverInputInfo; // undefined means no identified giver agent
|
giver?: GiverInputInfo; // undefined means no identified giver agent
|
||||||
description = "";
|
description = "";
|
||||||
givenToUser = false;
|
givenToUser = false;
|
||||||
|
isTrade = false;
|
||||||
|
offerId = "";
|
||||||
unitCode = "HUR";
|
unitCode = "HUR";
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
libsUtil = libsUtil;
|
||||||
UNIT_SHORT: Record<string, string> = {
|
|
||||||
"BTC": "BTC",
|
|
||||||
"ETH": "ETH",
|
|
||||||
"HUR": "Hours",
|
|
||||||
"USD": "US $",
|
|
||||||
};
|
|
||||||
/* eslint-enable prettier/prettier */
|
|
||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
async open(giver?: GiverInputInfo, offerId?: string) {
|
||||||
UNIT_LONG: Record<string, string> = {
|
this.description = "";
|
||||||
"BTC": "BTC",
|
this.giver = giver || {};
|
||||||
"ETH": "ETH",
|
// if we show "given to user" selection, default checkbox to true
|
||||||
"HUR": "hours",
|
this.givenToUser = this.showGivenToUser;
|
||||||
"USD": "dollars",
|
this.amountInput = "0";
|
||||||
};
|
this.offerId = offerId || "";
|
||||||
/* eslint-enable prettier/prettier */
|
|
||||||
|
|
||||||
async created() {
|
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
|
if (!this.giver.name) {
|
||||||
|
this.giver.name = didInfo(
|
||||||
|
this.giver.did,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
);
|
||||||
|
}
|
||||||
// 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 from database:", err);
|
console.error("Error retrieving settings from database:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -128,14 +150,6 @@ export default class GiftedDialog extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
open(giver: GiverInputInfo) {
|
|
||||||
this.description = "";
|
|
||||||
this.giver = giver;
|
|
||||||
// if we show "given to user" selection, default checkbox to true
|
|
||||||
this.givenToUser = this.showGivenToUser;
|
|
||||||
this.amountInput = "0";
|
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
@@ -146,7 +160,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changeUnitCode() {
|
changeUnitCode() {
|
||||||
const units = Object.keys(this.UNIT_SHORT);
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||||
const index = units.indexOf(this.unitCode);
|
const index = units.indexOf(this.unitCode);
|
||||||
this.unitCode = units[(index + 1) % units.length];
|
this.unitCode = units[(index + 1) % units.length];
|
||||||
}
|
}
|
||||||
@@ -172,6 +186,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.giver = undefined;
|
this.giver = undefined;
|
||||||
this.givenToUser = this.showGivenToUser;
|
this.givenToUser = this.showGivenToUser;
|
||||||
this.amountInput = "0";
|
this.amountInput = "0";
|
||||||
|
this.unitCode = "HUR";
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm() {
|
async confirm() {
|
||||||
@@ -187,7 +202,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
);
|
);
|
||||||
// this is asynchronous, but we don't need to wait for it to complete
|
// this is asynchronous, but we don't need to wait for it to complete
|
||||||
await this.recordGive(
|
await this.recordGive(
|
||||||
this.giver?.did as string | undefined,
|
(this.giver?.did as string) || null,
|
||||||
this.description,
|
this.description,
|
||||||
parseFloat(this.amountInput),
|
parseFloat(this.amountInput),
|
||||||
this.unitCode,
|
this.unitCode,
|
||||||
@@ -206,7 +221,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load Give records for DID ${activeDid} but no identity was found",
|
"Attempted to load Give records for DID ${activeDid} but no identifier was found",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
@@ -217,12 +232,13 @@ 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 amountInput may be 0
|
||||||
|
* @param unitCode may be omitted, defaults to "HUR"
|
||||||
*/
|
*/
|
||||||
public async recordGive(
|
public async recordGive(
|
||||||
giverDid?: string,
|
giverDid: string | null,
|
||||||
description?: string,
|
description: string,
|
||||||
amountInput?: number,
|
amountInput: number,
|
||||||
unitCode?: string,
|
unitCode: string = "HUR",
|
||||||
) {
|
) {
|
||||||
if (!this.activeDid) {
|
if (!this.activeDid) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -230,7 +246,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "You must select an identity before you can record a give.",
|
text: "You must select an identifier before you can record a give.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -243,9 +259,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
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 ${this.libsUtil.UNIT_LONG[unitCode]}.`,
|
||||||
this.UNIT_LONG[this.unitCode]
|
|
||||||
}.`,
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -264,6 +278,8 @@ export default class GiftedDialog extends Vue {
|
|||||||
amountInput,
|
amountInput,
|
||||||
unitCode,
|
unitCode,
|
||||||
this.projectId,
|
this.projectId,
|
||||||
|
this.offerId,
|
||||||
|
this.isTrade,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -271,7 +287,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.isGiveCreationError(result.response)
|
this.isGiveCreationError(result.response)
|
||||||
) {
|
) {
|
||||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||||
console.log("Error with give creation result:", result);
|
console.error("Error with give creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -287,14 +303,14 @@ export default class GiftedDialog extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success",
|
title: "Success",
|
||||||
text: "That gift was recorded.",
|
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
|
||||||
},
|
},
|
||||||
7000,
|
7000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log("Error with give recordation caught:", error);
|
console.error("Error with give recordation caught:", error);
|
||||||
const message =
|
const message =
|
||||||
error.userMessage ||
|
error.userMessage ||
|
||||||
error.response?.data?.error?.message ||
|
error.response?.data?.error?.message ||
|
||||||
|
|||||||
241
src/components/GiftedPrompts.vue
Normal file
241
src/components/GiftedPrompts.vue
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="dialog-overlay">
|
||||||
|
<div class="dialog">
|
||||||
|
<h1 class="text-xl font-bold text-center mb-4">Here's one:</h1>
|
||||||
|
<span class="flex justify-between">
|
||||||
|
<span
|
||||||
|
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
|
||||||
|
@click="prevIdea()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="m-auto" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="m-2">
|
||||||
|
<span v-if="currentIdeaIndex < IDEAS.length">
|
||||||
|
<p class="text-center text-lg font-bold">
|
||||||
|
{{ IDEAS[currentIdeaIndex] }}
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
<div v-if="currentIdeaIndex == IDEAS.length + 0">
|
||||||
|
<p class="text-center">
|
||||||
|
<span
|
||||||
|
v-if="currentContact == null"
|
||||||
|
class="text-orange-500 text-lg font-bold"
|
||||||
|
>
|
||||||
|
That's all your contacts.
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<span class="text-lg font-bold">
|
||||||
|
Did {{ currentContact.name || AppString.NO_CONTACT_NAME }}
|
||||||
|
<br />
|
||||||
|
or someone near them do anything – maybe a while ago?
|
||||||
|
</span>
|
||||||
|
<span class="flex justify-between">
|
||||||
|
<span />
|
||||||
|
<button
|
||||||
|
class="text-center bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4"
|
||||||
|
@click="nextIdeaPastContacts()"
|
||||||
|
>
|
||||||
|
Skip Contacts <fa icon="forward" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2 flex"
|
||||||
|
@click="nextIdea()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-right" class="m-auto" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4"
|
||||||
|
@click="cancel"
|
||||||
|
>
|
||||||
|
That's it!
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
import { AppString } from "@/constants/app";
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class GivenPrompts extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
IDEAS = [
|
||||||
|
"Did anyone fix food for you?",
|
||||||
|
"Did a family member do something for you?",
|
||||||
|
"Did anyone give you a compliment?",
|
||||||
|
"Who is someone you can always rely on, and how did they demonstrate that?",
|
||||||
|
"Did you see anyone give to someone else?",
|
||||||
|
"Is there someone who you have never met who has helped you somehow?",
|
||||||
|
"How did an artist or musician or author inspire you?",
|
||||||
|
"What inspiration did you get from someone who handled tragedy well?",
|
||||||
|
"Did some organization give something worth respect?",
|
||||||
|
"Who last gave you a good laugh?",
|
||||||
|
"Do you recall anything that was given to you while you were young?",
|
||||||
|
"Did someone forgive you or overlook a mistake?",
|
||||||
|
"Do you know of a way an ancestor contributed to your life?",
|
||||||
|
"Did anyone give you help at work?",
|
||||||
|
"How did a teacher or mentor or great example help you?",
|
||||||
|
];
|
||||||
|
OTHER_PROMPTS = 1;
|
||||||
|
CONTACT_PROMPT_INDEX = this.IDEAS.length; // expected after other prompts
|
||||||
|
|
||||||
|
currentContact: Contact | undefined = undefined;
|
||||||
|
currentIdeaIndex = 0;
|
||||||
|
numContacts = 0;
|
||||||
|
shownContactDbIndices: number[] = [];
|
||||||
|
visible = false;
|
||||||
|
|
||||||
|
AppString = AppString;
|
||||||
|
|
||||||
|
async open() {
|
||||||
|
this.visible = true;
|
||||||
|
|
||||||
|
await db.open();
|
||||||
|
this.numContacts = await db.contacts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
// close the dialog but don't change values (just in case some actions are added later)
|
||||||
|
this.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next idea.
|
||||||
|
* If it is a contact prompt, loop through.
|
||||||
|
*/
|
||||||
|
async nextIdea() {
|
||||||
|
// if we're incrementing to the contact prompt
|
||||||
|
// or if we're at the contact prompt and there was a previous contact...
|
||||||
|
if (
|
||||||
|
this.currentIdeaIndex == this.CONTACT_PROMPT_INDEX - 1 ||
|
||||||
|
(this.currentIdeaIndex == this.CONTACT_PROMPT_INDEX &&
|
||||||
|
this.shownContactDbIndices.length < this.numContacts)
|
||||||
|
) {
|
||||||
|
this.currentIdeaIndex = this.CONTACT_PROMPT_INDEX;
|
||||||
|
this.findNextUnshownContact();
|
||||||
|
} else {
|
||||||
|
// we're not at the contact prompt (or we ran out), so increment the idea index
|
||||||
|
this.currentIdeaIndex =
|
||||||
|
(this.currentIdeaIndex + 1) % (this.IDEAS.length + this.OTHER_PROMPTS);
|
||||||
|
// ... and clear out any other prompt info
|
||||||
|
this.currentContact = undefined;
|
||||||
|
this.shownContactDbIndices = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevIdea() {
|
||||||
|
if (
|
||||||
|
this.currentIdeaIndex ==
|
||||||
|
(this.CONTACT_PROMPT_INDEX + 1) %
|
||||||
|
(this.IDEAS.length + this.OTHER_PROMPTS) ||
|
||||||
|
(this.currentIdeaIndex == this.CONTACT_PROMPT_INDEX &&
|
||||||
|
this.shownContactDbIndices.length < this.numContacts)
|
||||||
|
) {
|
||||||
|
this.currentIdeaIndex = this.CONTACT_PROMPT_INDEX;
|
||||||
|
this.findNextUnshownContact();
|
||||||
|
} else {
|
||||||
|
// we're not at the contact prompt (or we ran out), so increment the idea index
|
||||||
|
this.currentIdeaIndex--;
|
||||||
|
if (this.currentIdeaIndex < 0) {
|
||||||
|
this.currentIdeaIndex = this.IDEAS.length - 1 + this.OTHER_PROMPTS;
|
||||||
|
}
|
||||||
|
// ... and clear out any other prompt info
|
||||||
|
this.currentContact = undefined;
|
||||||
|
this.shownContactDbIndices = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextIdeaPastContacts() {
|
||||||
|
this.currentIdeaIndex = 0;
|
||||||
|
this.currentContact = undefined;
|
||||||
|
this.shownContactDbIndices = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async findNextUnshownContact() {
|
||||||
|
// get a random contact
|
||||||
|
if (this.shownContactDbIndices.length === this.numContacts) {
|
||||||
|
// no more contacts to show
|
||||||
|
this.currentContact = undefined;
|
||||||
|
} else {
|
||||||
|
// get a random contact that hasn't been shown yet
|
||||||
|
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
||||||
|
// and guarantee that one is found by walking past shown contacts
|
||||||
|
let shownContactIndex =
|
||||||
|
this.shownContactDbIndices.indexOf(someContactDbIndex);
|
||||||
|
while (shownContactIndex !== -1) {
|
||||||
|
// increment both indices until we find a spot where "shown" skips a spot
|
||||||
|
shownContactIndex = (shownContactIndex + 1) % this.numContacts;
|
||||||
|
someContactDbIndex = (someContactDbIndex + 1) % this.numContacts;
|
||||||
|
if (
|
||||||
|
this.shownContactDbIndices[shownContactIndex] !== someContactDbIndex
|
||||||
|
) {
|
||||||
|
// we found a contact that hasn't been shown yet
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// continue
|
||||||
|
// ... and there must be at least one because shownContactDbIndices length < numContacts
|
||||||
|
}
|
||||||
|
this.shownContactDbIndices.push(someContactDbIndex);
|
||||||
|
this.shownContactDbIndices.sort();
|
||||||
|
|
||||||
|
// get the contact at that offset
|
||||||
|
await db.open();
|
||||||
|
this.currentContact = await db.contacts
|
||||||
|
.offset(someContactDbIndex)
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.currentContact = undefined;
|
||||||
|
this.currentIdeaIndex = 0;
|
||||||
|
this.numContacts = 0;
|
||||||
|
this.shownContactDbIndices = [];
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background-color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,22 +8,24 @@
|
|||||||
placeholder="Description, prerequisites, terms, etc."
|
placeholder="Description, prerequisites, terms, etc."
|
||||||
v-model="description"
|
v-model="description"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row mb-6">
|
<div class="flex flex-row mt-2">
|
||||||
<span
|
<span
|
||||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2"
|
||||||
|
@click="changeUnitCode()"
|
||||||
>
|
>
|
||||||
Hours
|
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
|
||||||
</span>
|
</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="number"
|
||||||
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="hours"
|
v-model="amountInput"
|
||||||
/>
|
/>
|
||||||
<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"
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
<fa icon="chevron-right" />
|
<fa icon="chevron-right" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row mb-6">
|
<div class="flex flex-row mt-2">
|
||||||
<span
|
<span
|
||||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
||||||
>
|
>
|
||||||
@@ -45,7 +47,9 @@
|
|||||||
v-model="expirationDateInput"
|
v-model="expirationDateInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
|
<p class="text-center mt-6 mb-2 italic">
|
||||||
|
Sign & Send to publish to the world
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
@click="confirm"
|
@click="confirm"
|
||||||
@@ -65,6 +69,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
import { createAndSubmitOffer } from "@/libs/endorserServer";
|
import { createAndSubmitOffer } from "@/libs/endorserServer";
|
||||||
|
import * as libsUtil from "@/libs/util";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
@@ -86,12 +91,15 @@ export default class OfferDialog extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
|
||||||
|
amountInput = "0";
|
||||||
|
amountUnitCode = "HUR";
|
||||||
description = "";
|
description = "";
|
||||||
expirationDateInput = "";
|
expirationDateInput = "";
|
||||||
hours = "0";
|
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
async created() {
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
|
async open() {
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
@@ -110,28 +118,41 @@ export default class OfferDialog extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
open() {
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
// close the dialog but don't change values (since it might be submitting info)
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeUnitCode() {
|
||||||
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||||
|
const index = units.indexOf(this.amountUnitCode);
|
||||||
|
this.amountUnitCode = units[(index + 1) % units.length];
|
||||||
|
}
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
decrement() {
|
decrement() {
|
||||||
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
this.amountInput = `${Math.max(
|
||||||
|
0,
|
||||||
|
(parseFloat(this.amountInput) || 1) - 1,
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.close();
|
this.close();
|
||||||
|
this.eraseValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
eraseValues() {
|
||||||
this.description = "";
|
this.description = "";
|
||||||
this.hours = "0";
|
this.amountInput = "0";
|
||||||
|
this.amountUnitCode = "HUR";
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm() {
|
async confirm() {
|
||||||
@@ -148,11 +169,12 @@ export default class OfferDialog extends Vue {
|
|||||||
// this is asynchronous, but we don't need to wait for it to complete
|
// this is asynchronous, but we don't need to wait for it to complete
|
||||||
this.recordOffer(
|
this.recordOffer(
|
||||||
this.description,
|
this.description,
|
||||||
parseFloat(this.hours),
|
parseFloat(this.amountInput),
|
||||||
|
this.amountUnitCode,
|
||||||
this.expirationDateInput,
|
this.expirationDateInput,
|
||||||
).then(() => {
|
).then(() => {
|
||||||
this.description = "";
|
this.description = "";
|
||||||
this.hours = "0";
|
this.amountInput = "0";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +188,7 @@ export default class OfferDialog extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load Offer records for DID ${activeDid} but no identity was found",
|
`Attempted to load Offer records for DID ${activeDid} but no identifier was found`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
@@ -176,10 +198,12 @@ export default class OfferDialog extends Vue {
|
|||||||
*
|
*
|
||||||
* @param description may be an empty string
|
* @param description may be an empty string
|
||||||
* @param hours may be 0
|
* @param hours may be 0
|
||||||
|
* @param unitCode may be omitted, defaults to "HUR"
|
||||||
*/
|
*/
|
||||||
public async recordOffer(
|
public async recordOffer(
|
||||||
description?: string,
|
description: string,
|
||||||
hours?: number,
|
amount: number,
|
||||||
|
unitCode: string = "HUR",
|
||||||
expirationDateInput?: string,
|
expirationDateInput?: string,
|
||||||
) {
|
) {
|
||||||
if (!this.activeDid) {
|
if (!this.activeDid) {
|
||||||
@@ -188,20 +212,20 @@ export default class OfferDialog extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "You must select an identity before you can record an offer.",
|
text: "You must select an identifier before you can record an offer.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description && !hours) {
|
if (!description && !amount) {
|
||||||
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 hours.",
|
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -215,7 +239,8 @@ export default class OfferDialog extends Vue {
|
|||||||
this.apiServer,
|
this.apiServer,
|
||||||
identity,
|
identity,
|
||||||
description,
|
description,
|
||||||
hours,
|
amount,
|
||||||
|
unitCode,
|
||||||
expirationDateInput,
|
expirationDateInput,
|
||||||
this.projectId,
|
this.projectId,
|
||||||
);
|
);
|
||||||
|
|||||||
32
src/components/ProjectIcon.vue
Normal file
32
src/components/ProjectIcon.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div v-html="generateIdenticon()" class="w-fit"></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { toSvg } from "jdenticon";
|
||||||
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
const BLANK_CONFIG = {
|
||||||
|
lightness: {
|
||||||
|
color: [1.0, 1.0],
|
||||||
|
grayscale: [1.0, 1.0],
|
||||||
|
},
|
||||||
|
saturation: {
|
||||||
|
color: 0.0,
|
||||||
|
grayscale: 0.0,
|
||||||
|
},
|
||||||
|
backColor: "#0000",
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class ProjectIcon extends Vue {
|
||||||
|
@Prop entityId = "";
|
||||||
|
@Prop iconSize = 0;
|
||||||
|
|
||||||
|
generateIdenticon() {
|
||||||
|
const config = this.entityId ? undefined : BLANK_CONFIG;
|
||||||
|
const svgString = toSvg(this.entityId, this.iconSize, config);
|
||||||
|
return svgString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
||||||
@@ -11,6 +11,8 @@ export enum AppString {
|
|||||||
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",
|
||||||
|
|
||||||
|
NO_CONTACT_NAME = "(no name)",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
|
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
|
||||||
|
|||||||
@@ -31,12 +31,15 @@ export type Settings = {
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
showContactGivesInline?: boolean; // Display contact inline or not
|
showContactGivesInline?: boolean; // Display contact inline or not
|
||||||
|
starredProjects?: Array<string>; // Array of starred project IDs
|
||||||
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
|
warnIfProdServer?: boolean; // Warn if using a production server
|
||||||
warnIfTestServer?: boolean; // Warn if using a testing server
|
warnIfTestServer?: boolean; // Warn if using a testing server
|
||||||
webPushServer?: string; // Web Push server URL
|
webPushServer?: string; // Web Push server URL
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: Settings = { id: 1 };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema for the Settings table in the database.
|
* Schema for the Settings table in the database.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Contact } from "@/db/tables/contacts";
|
|||||||
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||||
// the object in RegisterAction claims
|
// the object in RegisterAction claims
|
||||||
export const SERVICE_ID = "endorser.ch";
|
export const SERVICE_ID = "endorser.ch";
|
||||||
|
// the header line for contacts exported via Endorser Mobile
|
||||||
|
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
|
||||||
// the prefix for the contact URL
|
// the prefix for the contact URL
|
||||||
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
|
||||||
@@ -32,7 +34,8 @@ export interface GiverOutputInfo {
|
|||||||
action: string;
|
action: string;
|
||||||
giver?: GiverInputInfo;
|
giver?: GiverInputInfo;
|
||||||
description?: string;
|
description?: string;
|
||||||
hours?: number;
|
amount?: number;
|
||||||
|
unitCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaimResult {
|
export interface ClaimResult {
|
||||||
@@ -52,6 +55,7 @@ export interface GenericServerRecord extends GenericVerifiableCredential {
|
|||||||
issuer?: 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>;
|
||||||
|
claimType?: string;
|
||||||
}
|
}
|
||||||
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
|
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
@@ -65,8 +69,10 @@ export interface GiveServerRecord {
|
|||||||
amountConfirmed: number;
|
amountConfirmed: number;
|
||||||
description: string;
|
description: string;
|
||||||
fullClaim: GiveVerifiableCredential;
|
fullClaim: GiveVerifiableCredential;
|
||||||
|
fulfillsPlanHandleId: string;
|
||||||
handleId: string;
|
handleId: string;
|
||||||
issuedAt: string;
|
issuedAt: string;
|
||||||
|
jwtId: string;
|
||||||
recipientDid: string;
|
recipientDid: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
}
|
}
|
||||||
@@ -74,6 +80,13 @@ export interface GiveServerRecord {
|
|||||||
export interface OfferServerRecord {
|
export interface OfferServerRecord {
|
||||||
amount: number;
|
amount: number;
|
||||||
amountGiven: number;
|
amountGiven: number;
|
||||||
|
amountGivenConfirmed: number;
|
||||||
|
fullClaim: OfferVerifiableCredential;
|
||||||
|
fulfillsPlanHandleId: string;
|
||||||
|
handleId: string;
|
||||||
|
jwtId: string;
|
||||||
|
nonAmountGivenConfirmed: number;
|
||||||
|
objectDescription: string;
|
||||||
offeredByDid: string;
|
offeredByDid: string;
|
||||||
recipientDid: string;
|
recipientDid: string;
|
||||||
requirementsMet: boolean;
|
requirementsMet: boolean;
|
||||||
@@ -81,6 +94,20 @@ export interface OfferServerRecord {
|
|||||||
validThrough: string;
|
validThrough: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlanServerRecord {
|
||||||
|
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
||||||
|
description: string;
|
||||||
|
endTime?: string;
|
||||||
|
fulfillsPlanHandleId: string;
|
||||||
|
issuerDid: string;
|
||||||
|
handleId: string;
|
||||||
|
locLat?: number;
|
||||||
|
locLon?: number;
|
||||||
|
name: string;
|
||||||
|
startTime?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id4
|
// https://endorser.ch/doc/html/transactions.html#id4
|
||||||
export interface GiveVerifiableCredential {
|
export interface GiveVerifiableCredential {
|
||||||
@@ -88,7 +115,7 @@ export interface GiveVerifiableCredential {
|
|||||||
"@type": "GiveAction";
|
"@type": "GiveAction";
|
||||||
agent?: { identifier: string };
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string };
|
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
object?: { amountOfThisGood: number; unitCode: string };
|
object?: { amountOfThisGood: number; unitCode: string };
|
||||||
recipient?: { identifier: string };
|
recipient?: { identifier: string };
|
||||||
@@ -115,23 +142,64 @@ export interface PlanVerifiableCredential {
|
|||||||
"@context": "https://schema.org";
|
"@context": "https://schema.org";
|
||||||
"@type": "PlanAction";
|
"@type": "PlanAction";
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
agent?: { identifier: string };
|
||||||
|
description?: string;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
|
lastClaimId?: string;
|
||||||
location?: {
|
location?: {
|
||||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanServerRecord {
|
/**
|
||||||
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
* Represents data about a project
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
* We should use PlanServerRecord instead.
|
||||||
|
**/
|
||||||
|
export interface PlanData {
|
||||||
|
/**
|
||||||
|
* Name of the project
|
||||||
|
**/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Description of the project
|
||||||
|
**/
|
||||||
description: string;
|
description: string;
|
||||||
endTime?: string;
|
/**
|
||||||
issuerDid: string;
|
* URL referencing information about the project
|
||||||
|
**/
|
||||||
handleId: string;
|
handleId: string;
|
||||||
locLat?: number;
|
/**
|
||||||
locLon?: number;
|
* The DID of the issuer
|
||||||
|
*/
|
||||||
|
issuerDid: string;
|
||||||
|
/**
|
||||||
|
* The Identier of the project -- different from jwtId, needs to be fixed
|
||||||
|
**/
|
||||||
|
rowid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimits {
|
||||||
|
doneClaimsThisWeek: string;
|
||||||
|
doneRegistrationsThisMonth: string;
|
||||||
|
maxClaimsPerWeek: string;
|
||||||
|
maxRegistrationsPerMonth: string;
|
||||||
|
nextMonthBeginDateTime: string;
|
||||||
|
nextWeekBeginDateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifiableCredential {
|
||||||
|
"@context": string;
|
||||||
|
"@type": string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
identifier?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorldProperties {
|
||||||
startTime?: string;
|
startTime?: string;
|
||||||
url?: string;
|
endTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterVerifiableCredential {
|
export interface RegisterVerifiableCredential {
|
||||||
@@ -142,40 +210,72 @@ export interface RegisterVerifiableCredential {
|
|||||||
participant: { identifier: string };
|
participant: { identifier: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// now for some of the error & other wrapper types
|
||||||
|
|
||||||
|
export interface ResultWithType {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuccessResult extends ResultWithType {
|
||||||
|
type: "success";
|
||||||
|
response: AxiosResponse<ClaimResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error?: {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResult {
|
||||||
|
type: "error";
|
||||||
|
error: InternalError;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InternalError {
|
export interface InternalError {
|
||||||
error: string; // for system logging
|
error: string; // for system logging
|
||||||
userMessage?: string; // for user display
|
userMessage?: string; // for user display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||||
|
|
||||||
// This is used to check for hidden info.
|
// This is used to check for hidden info.
|
||||||
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||||
const HIDDEN_DID = "did:none:HIDDEN";
|
const HIDDEN_DID = "did:none:HIDDEN";
|
||||||
|
|
||||||
|
export function isDid(did: string) {
|
||||||
|
return did.startsWith("did:");
|
||||||
|
}
|
||||||
|
|
||||||
export function isHiddenDid(did: string) {
|
export function isHiddenDid(did: string) {
|
||||||
return did === HIDDEN_DID;
|
return did === HIDDEN_DID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEmptyOrHiddenDid(did?: string) {
|
||||||
|
return !did || did === HIDDEN_DID; // catching empty string as well
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true for any nested string where func(input) === true
|
* @return true for any nested string where func(input) === true
|
||||||
*
|
*
|
||||||
* Similar logic is found in endorser-mobile.
|
* Similar logic is found in endorser-mobile.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function testRecursivelyOnString(func: (arg0: any) => boolean, input: any) {
|
function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
|
||||||
if (Object.prototype.toString.call(input) === "[object String]") {
|
if (Object.prototype.toString.call(input) === "[object String]") {
|
||||||
return func(input);
|
return func(input);
|
||||||
} else if (input instanceof Object) {
|
} else if (input instanceof Object) {
|
||||||
if (!Array.isArray(input)) {
|
if (!Array.isArray(input)) {
|
||||||
// it's an object
|
// it's an object
|
||||||
for (const key in input) {
|
for (const key in input) {
|
||||||
if (testRecursivelyOnString(func, input[key])) {
|
if (testRecursivelyOnStrings(func, input[key])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// it's an array
|
// it's an array
|
||||||
for (const value of input) {
|
for (const value of input) {
|
||||||
if (testRecursivelyOnString(func, value)) {
|
if (testRecursivelyOnStrings(func, value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +288,7 @@ function testRecursivelyOnString(func: (arg0: any) => boolean, input: any) {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function containsHiddenDid(obj: any) {
|
export function containsHiddenDid(obj: any) {
|
||||||
return testRecursivelyOnString(isHiddenDid, obj);
|
return testRecursivelyOnStrings(isHiddenDid, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stripEndorserPrefix(claimId: string) {
|
export function stripEndorserPrefix(claimId: string) {
|
||||||
@@ -259,59 +359,47 @@ export function removeVisibleToDids(input: any): any {
|
|||||||
Similar logic is found in endorser-mobile.
|
Similar logic is found in endorser-mobile.
|
||||||
**/
|
**/
|
||||||
export function didInfo(
|
export function didInfo(
|
||||||
did: string,
|
did: string | undefined,
|
||||||
activeDid: string,
|
activeDid: string | undefined,
|
||||||
allMyDids: string[],
|
allMyDids: string[],
|
||||||
contacts: Contact[],
|
contacts: Contact[],
|
||||||
): string {
|
): string {
|
||||||
if (!did) return "Someone Anonymous";
|
if (!did) return "Someone Anonymous";
|
||||||
|
|
||||||
const myId = R.find(R.equals(did), allMyDids);
|
|
||||||
if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`;
|
|
||||||
|
|
||||||
const contact = R.find((c) => c.did === did, contacts);
|
const contact = R.find((c) => c.did === did, contacts);
|
||||||
return contact
|
if (contact) {
|
||||||
? contact.name || "Contact With No Name"
|
return contact.name || "Contact With No Name";
|
||||||
: isHiddenDid(did)
|
} else {
|
||||||
? "Someone Not In Network"
|
const myId = R.find(R.equals(did), allMyDids);
|
||||||
: "Someone Not In Contacts";
|
return myId
|
||||||
|
? `You${myId !== activeDid ? " (Alt ID)" : ""}`
|
||||||
|
: isHiddenDid(did)
|
||||||
|
? "Someone Not In Network"
|
||||||
|
: "Someone Not In Contacts";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultWithType {
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SuccessResult extends ResultWithType {
|
|
||||||
type: "success";
|
|
||||||
response: AxiosResponse<ClaimResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorResult {
|
|
||||||
type: "error";
|
|
||||||
error: InternalError;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
* @param identity
|
* @param identity
|
||||||
* @param fromDid may be null
|
* @param fromDid may be null
|
||||||
* @param toDid
|
* @param toDid
|
||||||
* @param description may be null; should have this or hours
|
* @param description may be null; should have this or amount
|
||||||
* @param hours may be null; should have this or description
|
* @param amount may be null; should have this or description
|
||||||
*/
|
*/
|
||||||
export async function createAndSubmitGive(
|
export async function createAndSubmitGive(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
apiServer: string,
|
apiServer: string,
|
||||||
identity: IIdentifier,
|
identity: IIdentifier,
|
||||||
fromDid?: string,
|
fromDid?: string | null,
|
||||||
toDid?: string,
|
toDid?: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
hours?: number,
|
amount?: number,
|
||||||
unitCode?: string,
|
unitCode?: string,
|
||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
|
fulfillsOfferHandleId?: string,
|
||||||
|
isTrade: boolean = false,
|
||||||
): Promise<CreateAndSubmitClaimResult> {
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
const vcClaim: GiveVerifiableCredential = {
|
const vcClaim: GiveVerifiableCredential = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -319,13 +407,25 @@ 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: amount
|
||||||
? { amountOfThisGood: hours, unitCode: unitCode || "HUR" }
|
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||||
: undefined,
|
|
||||||
fulfills: fulfillsProjectHandleId
|
|
||||||
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
|
||||||
: undefined,
|
: undefined,
|
||||||
|
fulfills: [{ "@type": isTrade ? "TradeAction" : "DonateAction" }],
|
||||||
};
|
};
|
||||||
|
if (fulfillsProjectHandleId) {
|
||||||
|
vcClaim.fulfills = vcClaim.fulfills || []; // weird that it won't typecheck without this
|
||||||
|
vcClaim.fulfills.push({
|
||||||
|
"@type": "PlanAction",
|
||||||
|
identifier: fulfillsProjectHandleId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fulfillsOfferHandleId) {
|
||||||
|
vcClaim.fulfills = vcClaim.fulfills || []; // weird that it won't typecheck without this
|
||||||
|
vcClaim.fulfills.push({
|
||||||
|
"@type": "Offer",
|
||||||
|
identifier: fulfillsOfferHandleId,
|
||||||
|
});
|
||||||
|
}
|
||||||
return createAndSubmitClaim(
|
return createAndSubmitClaim(
|
||||||
vcClaim as GenericServerRecord,
|
vcClaim as GenericServerRecord,
|
||||||
identity,
|
identity,
|
||||||
@@ -338,8 +438,8 @@ export async function createAndSubmitGive(
|
|||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
* @param identity
|
* @param identity
|
||||||
* @param description may be null; should have this or hours
|
* @param description may be null; should have this or amount
|
||||||
* @param hours may be null; should have this or description
|
* @param amount may be null; should have this or description
|
||||||
* @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
|
* @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
|
||||||
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
||||||
*/
|
*/
|
||||||
@@ -348,7 +448,8 @@ export async function createAndSubmitOffer(
|
|||||||
apiServer: string,
|
apiServer: string,
|
||||||
identity: IIdentifier,
|
identity: IIdentifier,
|
||||||
description?: string,
|
description?: string,
|
||||||
hours?: number,
|
amount?: number,
|
||||||
|
unitCode?: string,
|
||||||
expirationDate?: string,
|
expirationDate?: string,
|
||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
): Promise<CreateAndSubmitClaimResult> {
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
@@ -358,10 +459,10 @@ export async function createAndSubmitOffer(
|
|||||||
offeredBy: { identifier: identity.did },
|
offeredBy: { identifier: identity.did },
|
||||||
validThrough: expirationDate || undefined,
|
validThrough: expirationDate || undefined,
|
||||||
};
|
};
|
||||||
if (hours) {
|
if (amount) {
|
||||||
vcClaim.includesObject = {
|
vcClaim.includesObject = {
|
||||||
amountOfThisGood: hours,
|
amountOfThisGood: amount,
|
||||||
unitCode: "HUR",
|
unitCode: unitCode || "HUR",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (description) {
|
if (description) {
|
||||||
@@ -431,15 +532,16 @@ export async function createAndSubmitClaim(
|
|||||||
return { type: "success", response };
|
return { type: "success", response };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log("Error creating claim:", error);
|
console.error("Error creating claim:", error);
|
||||||
const errorMessage: string =
|
const errorMessage: string =
|
||||||
error.response?.data?.error?.message || error.message || "Unknown error";
|
error.response?.data?.error?.message ||
|
||||||
|
error.message ||
|
||||||
|
"Got some error submitting the claim. Check your permissions, network, and error logs.";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
error: {
|
error: {
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
userMessage: "Failed to create and submit the claim.",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -454,57 +556,3 @@ export function isNumeric(str: string): boolean {
|
|||||||
export function numberOrZero(str: string): number {
|
export function numberOrZero(str: string): number {
|
||||||
return isNumeric(str) ? +str : 0;
|
return isNumeric(str) ? +str : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorResponse {
|
|
||||||
error?: {
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RateLimits {
|
|
||||||
doneClaimsThisWeek: string;
|
|
||||||
doneRegistrationsThisMonth: string;
|
|
||||||
maxClaimsPerWeek: string;
|
|
||||||
maxRegistrationsPerMonth: string;
|
|
||||||
nextMonthBeginDateTime: string;
|
|
||||||
nextWeekBeginDateTime: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents data about a project
|
|
||||||
**/
|
|
||||||
export interface ProjectData {
|
|
||||||
/**
|
|
||||||
* Name of the project
|
|
||||||
**/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* Description of the project
|
|
||||||
**/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* URL referencing information about the project
|
|
||||||
**/
|
|
||||||
handleId: string;
|
|
||||||
/**
|
|
||||||
* The DID of the issuer
|
|
||||||
*/
|
|
||||||
issuerDid: string;
|
|
||||||
/**
|
|
||||||
* The Identier of the project
|
|
||||||
**/
|
|
||||||
rowid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerifiableCredential {
|
|
||||||
"@context": string;
|
|
||||||
"@type": string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
identifier?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorldProperties {
|
|
||||||
startTime?: string;
|
|
||||||
endTime?: string;
|
|
||||||
}
|
|
||||||
|
|||||||
199
src/libs/util.ts
199
src/libs/util.ts
@@ -3,16 +3,213 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||||
import { db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
|
||||||
|
import { GenericServerRecord, containsHiddenDid } from "@/libs/endorserServer";
|
||||||
|
import * as serverUtil from "@/libs/endorserServer";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
|
||||||
|
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, too),
|
||||||
|
// and make sure they can take all actions while the notification shows.
|
||||||
|
export const ONBOARD_MESSAGE =
|
||||||
|
"1) Check that they have entered their name on the profile page in their device. 2) Add them to your Contacts by scanning with the QR icon that is by the input box. 3) Click the person icon to register them. 4) Have them go to their Contact page and scan your QR to add you to their list.";
|
||||||
|
|
||||||
|
/* eslint-disable prettier/prettier */
|
||||||
|
export const UNIT_SHORT: Record<string, string> = {
|
||||||
|
"BTC": "BTC",
|
||||||
|
"ETH": "ETH",
|
||||||
|
"HUR": "Hours",
|
||||||
|
"USD": "US $",
|
||||||
|
};
|
||||||
|
/* eslint-enable prettier/prettier */
|
||||||
|
|
||||||
|
/* eslint-disable prettier/prettier */
|
||||||
|
export const UNIT_LONG: Record<string, string> = {
|
||||||
|
"BTC": "Bitcoin",
|
||||||
|
"ETH": "Ethereum",
|
||||||
|
"HUR": "hours",
|
||||||
|
"USD": "dollars",
|
||||||
|
};
|
||||||
|
/* eslint-enable prettier/prettier */
|
||||||
|
|
||||||
|
const UNIT_CODES: Record<string, Record<string, string>> = {
|
||||||
|
BTC: {
|
||||||
|
name: "Bitcoin",
|
||||||
|
faIcon: "bitcoin-sign",
|
||||||
|
},
|
||||||
|
HUR: {
|
||||||
|
name: "hours",
|
||||||
|
faIcon: "clock",
|
||||||
|
},
|
||||||
|
USD: {
|
||||||
|
name: "US Dollars",
|
||||||
|
faIcon: "dollar",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function iconForUnitCode(unitCode: string) {
|
||||||
|
return UNIT_CODES[unitCode]?.faIcon || "question";
|
||||||
|
}
|
||||||
|
|
||||||
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+.-]+:/));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const giveIsConfirmable = (veriClaim: GenericServerRecord) => {
|
||||||
|
return veriClaim.claimType === "GiveAction";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
||||||
|
fn();
|
||||||
|
useClipboard()
|
||||||
|
.copy(text)
|
||||||
|
.then(() => setTimeout(fn, 2000));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the user can confirm the claim
|
||||||
|
* @param veriClaim is expected to have fields: claim, claimType, and issuer
|
||||||
|
*/
|
||||||
|
export const isGiveRecordTheUserCanConfirm = (
|
||||||
|
veriClaim: GenericServerRecord,
|
||||||
|
activeDid: string,
|
||||||
|
confirmerIdList: string[] = [],
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
giveIsConfirmable(veriClaim) &&
|
||||||
|
!confirmerIdList.includes(activeDid) &&
|
||||||
|
veriClaim.issuer !== activeDid &&
|
||||||
|
!containsHiddenDid(veriClaim.claim)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the DID of the person who offered, or undefined if hidden
|
||||||
|
* @param veriClaim is expected to have fields: claim and issuer
|
||||||
|
*/
|
||||||
|
export const offerGiverDid: (
|
||||||
|
arg0: GenericServerRecord,
|
||||||
|
) => string | undefined = (veriClaim) => {
|
||||||
|
let giver;
|
||||||
|
if (
|
||||||
|
veriClaim.claim.offeredBy?.identifier &&
|
||||||
|
!serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
|
||||||
|
) {
|
||||||
|
giver = veriClaim.claim.offeredBy.identifier;
|
||||||
|
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
|
||||||
|
giver = veriClaim.issuer;
|
||||||
|
}
|
||||||
|
return giver;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns true if the user can fulfill the offer
|
||||||
|
* @param veriClaim is expected to have fields: claim, claimType, and issuer
|
||||||
|
*/
|
||||||
|
export const canFulfillOffer = (veriClaim: GenericServerRecord) => {
|
||||||
|
return !!(veriClaim.claimType === "Offer" && offerGiverDid(veriClaim));
|
||||||
|
};
|
||||||
|
|
||||||
|
// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
|
||||||
|
export function findAllVisibleToDids(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
input: any,
|
||||||
|
humanReadable = false,
|
||||||
|
): Record<string, Array<string>> {
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
const result: Record<string, Array<string>> = {};
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const inside = findAllVisibleToDids(input[i], humanReadable);
|
||||||
|
for (const key in inside) {
|
||||||
|
const pathKey = humanReadable
|
||||||
|
? "#" + (i + 1) + " " + key
|
||||||
|
: "[" + i + "]" + key;
|
||||||
|
result[pathKey] = inside[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else if (input instanceof Object) {
|
||||||
|
// regular map (non-array) object
|
||||||
|
const result: Record<string, Array<string>> = {};
|
||||||
|
for (const key in input) {
|
||||||
|
if (key.endsWith("VisibleToDids")) {
|
||||||
|
const newKey = key.slice(0, -"VisibleToDids".length);
|
||||||
|
const pathKey = humanReadable ? newKey : "." + newKey;
|
||||||
|
result[pathKey] = input[key];
|
||||||
|
} else {
|
||||||
|
const inside = findAllVisibleToDids(input[key], humanReadable);
|
||||||
|
for (const insideKey in inside) {
|
||||||
|
const pathKey = humanReadable
|
||||||
|
? key + "'s " + insideKey
|
||||||
|
: "." + key + insideKey;
|
||||||
|
result[pathKey] = inside[insideKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test findAllVisibleToDids
|
||||||
|
*
|
||||||
|
|
||||||
|
pkgx +deno.land sh
|
||||||
|
|
||||||
|
deno
|
||||||
|
|
||||||
|
import * as R from 'ramda';
|
||||||
|
//import { findAllVisibleToDids } from './src/libs/util'; // doesn't work because other dependencies fail so gotta copy-and-paste function
|
||||||
|
|
||||||
|
console.log(R.equals(findAllVisibleToDids(null), {}));
|
||||||
|
console.log(R.equals(findAllVisibleToDids(9), {}));
|
||||||
|
console.log(R.equals(findAllVisibleToDids([]), {}));
|
||||||
|
console.log(R.equals(findAllVisibleToDids({}), {}));
|
||||||
|
console.log(R.equals(findAllVisibleToDids({ issuer: "abc" }), {}));
|
||||||
|
console.log(R.equals(findAllVisibleToDids({ issuerVisibleToDids: ["abc"] }), { ".issuer": ["abc"] }));
|
||||||
|
console.log(R.equals(findAllVisibleToDids([{ issuerVisibleToDids: ["abc"] }]), { "[0].issuer": ["abc"] }));
|
||||||
|
console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] } }]), { "[1].fluff.issuer": ["abc"] }));
|
||||||
|
console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] }, stuff: [ { did: "HIDDEN", agentDidVisibleToDids: ["def", "ghi"] } ] }]), { "[1].fluff.issuer": ["abc"], "[1].stuff[0].agentDid": ["def", "ghi"] }));
|
||||||
|
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new identity, saves it to the database, and sets it as the active identity.
|
||||||
|
* @return {Promise<string>} with the DID of the new identity
|
||||||
|
*/
|
||||||
|
export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
||||||
|
const mnemonic = generateSeed();
|
||||||
|
// address is 0x... ETH address, without "did:eth:"
|
||||||
|
const [address, privateHex, publicHex, derivationPath] =
|
||||||
|
deriveAddress(mnemonic);
|
||||||
|
|
||||||
|
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||||
|
const identity = JSON.stringify(newId);
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
await accountsDB.accounts.add({
|
||||||
|
dateCreated: new Date().toISOString(),
|
||||||
|
derivationPath: derivationPath,
|
||||||
|
did: newId.did,
|
||||||
|
identity: identity,
|
||||||
|
mnemonic: mnemonic,
|
||||||
|
publicKeyHex: newId.keys[0].publicKeyHex,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: newId.did,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newId.did;
|
||||||
|
};
|
||||||
|
|
||||||
export const sendTestThroughPushServer = async (
|
export const sendTestThroughPushServer = async (
|
||||||
subscription: PushSubscription,
|
subscription: PushSubscription,
|
||||||
skipFilter: boolean,
|
skipFilter: boolean,
|
||||||
|
|||||||
10
src/main.ts
10
src/main.ts
@@ -36,15 +36,19 @@ import {
|
|||||||
faFileLines,
|
faFileLines,
|
||||||
faFloppyDisk,
|
faFloppyDisk,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
|
faForward,
|
||||||
faGift,
|
faGift,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
|
faHammer,
|
||||||
faHand,
|
faHand,
|
||||||
|
faHandHoldingHeart,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
faLocationDot,
|
faLocationDot,
|
||||||
faLongArrowAltLeft,
|
faLongArrowAltLeft,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faMessage,
|
faMessage,
|
||||||
|
faMinus,
|
||||||
faPen,
|
faPen,
|
||||||
faPersonCircleCheck,
|
faPersonCircleCheck,
|
||||||
faPersonCircleQuestion,
|
faPersonCircleQuestion,
|
||||||
@@ -57,6 +61,7 @@ import {
|
|||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
faSquarePlus,
|
faSquarePlus,
|
||||||
|
faStar,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
@@ -90,15 +95,19 @@ library.add(
|
|||||||
faFileLines,
|
faFileLines,
|
||||||
faFloppyDisk,
|
faFloppyDisk,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
|
faForward,
|
||||||
faGift,
|
faGift,
|
||||||
faGlobe,
|
faGlobe,
|
||||||
|
faHammer,
|
||||||
faHand,
|
faHand,
|
||||||
|
faHandHoldingHeart,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
faLocationDot,
|
faLocationDot,
|
||||||
faLongArrowAltLeft,
|
faLongArrowAltLeft,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faMessage,
|
faMessage,
|
||||||
|
faMinus,
|
||||||
faPen,
|
faPen,
|
||||||
faPersonCircleCheck,
|
faPersonCircleCheck,
|
||||||
faPersonCircleQuestion,
|
faPersonCircleQuestion,
|
||||||
@@ -111,6 +120,7 @@ library.add(
|
|||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
faSquarePlus,
|
faSquarePlus,
|
||||||
|
faStar,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { register } from "register-service-worker";
|
import { register } from "register-service-worker";
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
register("/additional-scripts.js", {
|
register("/sw_scripts-combined.js", {
|
||||||
ready() {
|
ready() {
|
||||||
console.log(
|
console.log(
|
||||||
"App is being served from cache by a service worker.\n" +
|
"App is being served from cache by a service worker.\n" +
|
||||||
|
|||||||
@@ -136,14 +136,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "new-edit-account" */ "../views/NewEditAccountView.vue"
|
/* webpackChunkName: "new-edit-account" */ "../views/NewEditAccountView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/new-edit-commitment",
|
|
||||||
name: "new-edit-commitment",
|
|
||||||
component: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "new-edit-commitment" */ "../views/NewEditCommitmentView.vue"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/new-edit-project",
|
path: "/new-edit-project",
|
||||||
name: "new-edit-project",
|
name: "new-edit-project",
|
||||||
|
|||||||
@@ -4,6 +4,16 @@
|
|||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Back -->
|
||||||
|
<div class="text-lg text-center font-light relative px-7">
|
||||||
|
<h1
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
@click="$router.back()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
||||||
Your Identity
|
Your Identity
|
||||||
@@ -40,16 +50,49 @@
|
|||||||
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"
|
||||||
>
|
>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
<b>Note:</b> Before you can take any action, you need an ID.
|
<b>Note:</b> Before you can share with others or take any action, you
|
||||||
|
need an identifier.
|
||||||
</p>
|
</p>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'start' }"
|
:to="{ name: 'start' }"
|
||||||
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Generate Identity
|
Create An Identifier
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Identity Details -->
|
||||||
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
|
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
||||||
|
{{ givenName }}
|
||||||
|
<router-link :to="{ name: 'new-edit-account' }">
|
||||||
|
<fa icon="pen" class="text-xs text-blue-500 mb-1"></fa>
|
||||||
|
</router-link>
|
||||||
|
</h2>
|
||||||
|
<span v-else>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'new-edit-account' }"
|
||||||
|
class="block w-full text-center text-md bg-amber-200 text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Set Your Name
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="text-slate-500 text-sm font-bold">ID</div>
|
||||||
|
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||||
|
<code class="truncate">{{ activeDid }}</code>
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
|
||||||
|
"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
<span v-show="showDidCopy">Copied</span>
|
||||||
|
</div>
|
||||||
|
</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 because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
|
||||||
<div
|
<div
|
||||||
@@ -68,38 +111,6 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Identity Details -->
|
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
|
||||||
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
|
||||||
{{ givenName }}
|
|
||||||
<router-link :to="{ name: 'new-edit-account' }">
|
|
||||||
<fa icon="pen" class="text-xs text-blue-500 mb-1"></fa>
|
|
||||||
</router-link>
|
|
||||||
</h2>
|
|
||||||
<span v-else>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'new-edit-account' }"
|
|
||||||
class="block w-full text-center text-md text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
|
||||||
>
|
|
||||||
(Set Your Name)
|
|
||||||
</router-link>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">ID</div>
|
|
||||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
|
||||||
<code class="truncate">{{ activeDid }}</code>
|
|
||||||
<button
|
|
||||||
@click="
|
|
||||||
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
|
|
||||||
"
|
|
||||||
class="ml-2"
|
|
||||||
>
|
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
||||||
</button>
|
|
||||||
<span v-show="showDidCopy">Copied!</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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
|
<div
|
||||||
v-if="!notificationMaybeChanged"
|
v-if="!notificationMaybeChanged"
|
||||||
@@ -134,41 +145,11 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data Export</h3>
|
<div
|
||||||
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'seed-backup' }"
|
|
||||||
v-if="activeDid"
|
v-if="activeDid"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||||
>
|
>
|
||||||
Backup Identifier Seed
|
<div class="mb-2">Usage Limits</div>
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-bind:class="computedStartDownloadLinkClassNames()"
|
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
|
||||||
@click="exportDatabase()"
|
|
||||||
>
|
|
||||||
Download Settings & Contacts
|
|
||||||
<br />
|
|
||||||
(excluding Identifier Data)
|
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
ref="downloadLink"
|
|
||||||
v-bind:class="computedDownloadLinkClassNames()"
|
|
||||||
class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
|
|
||||||
>
|
|
||||||
If no download happened yet, click again here to download now.
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div v-if="activeDid" class="flex mt-8 py-2">
|
|
||||||
<h3 class="text-sm uppercase font-semibold">Rate Limits</h3>
|
|
||||||
<button
|
|
||||||
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md ml-2 mr-2 mb-2"
|
|
||||||
@click="checkLimits()"
|
|
||||||
>
|
|
||||||
Check Limits
|
|
||||||
</button>
|
|
||||||
<!-- show spinner if loading limits -->
|
<!-- show spinner if loading limits -->
|
||||||
<div v-if="loadingLimits" class="text-center">
|
<div v-if="loadingLimits" class="text-center">
|
||||||
Checking… <fa icon="spinner" class="fa-spin"></fa>
|
Checking… <fa icon="spinner" class="fa-spin"></fa>
|
||||||
@@ -199,6 +180,40 @@
|
|||||||
</b>
|
</b>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
class="block float-right w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
||||||
|
@click="checkLimits()"
|
||||||
|
>
|
||||||
|
Recheck Limits
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||||
|
<div>Data Export</div>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'seed-backup' }"
|
||||||
|
v-if="activeDid"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
||||||
|
>
|
||||||
|
Backup Identifier Seed
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-bind:class="computedStartDownloadLinkClassNames()"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
||||||
|
@click="exportDatabase()"
|
||||||
|
>
|
||||||
|
Download Settings & Contacts
|
||||||
|
<br />
|
||||||
|
(excluding Identifier Data)
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
ref="downloadLink"
|
||||||
|
v-bind:class="computedDownloadLinkClassNames()"
|
||||||
|
class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
If no download happened yet, click again here to download now.
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
<!-- id used by puppeteer test script -->
|
||||||
@@ -216,9 +231,9 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Deep Identity Details -->
|
<!-- Deep Identity Details -->
|
||||||
<h2 class="text-sm uppercase font-semibold mb-3">
|
<span class="text-slate-500 text-sm font-bold mb-2">
|
||||||
Deep Identity Details
|
Deep Identifier Details
|
||||||
</h2>
|
</span>
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
||||||
<div
|
<div
|
||||||
@@ -233,7 +248,7 @@
|
|||||||
>
|
>
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
<span v-show="showB64Copy">Copied!</span>
|
<span v-show="showB64Copy">Copied</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
||||||
@@ -249,7 +264,7 @@
|
|||||||
>
|
>
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
<span v-show="showPubCopy">Copied!</span>
|
<span v-show="showPubCopy">Copied</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
||||||
@@ -268,17 +283,27 @@
|
|||||||
>
|
>
|
||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
<span v-show="showDerCopy">Copied!</span>
|
<span v-show="showDerCopy">Copied</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- id used by puppeteer test script -->
|
||||||
|
<router-link
|
||||||
|
id="switch-identity-link"
|
||||||
|
:to="{ name: 'identity-switcher' }"
|
||||||
|
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Switch Identifier
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
for="toggleShowAmounts"
|
for="toggleShowAmounts"
|
||||||
class="flex items-center justify-between cursor-pointer my-4"
|
class="flex items-center justify-between cursor-pointer my-4"
|
||||||
@click="handleChange"
|
@click="handleChange"
|
||||||
>
|
>
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<h2>Show amounts given with contacts</h2>
|
<span class="text-slate-500 text-sm font-bold">Contacts Display</span>
|
||||||
|
<span class="ml-2">Show amounts given</span>
|
||||||
<!-- toggle -->
|
<!-- toggle -->
|
||||||
<div class="relative ml-2">
|
<div class="relative ml-2">
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
@@ -297,119 +322,86 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="grid-cols-2 mb-4">
|
<div>
|
||||||
<span class="text-slate-500 text-sm font-bold mb-2">Data Import</span>
|
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
|
||||||
<input type="file" @change="uploadFile" class="ml-2" />
|
<div class="px-4 py-4">
|
||||||
<div v-if="showContactImport()">
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full rounded border border-slate-400 px-4 py-2"
|
||||||
|
v-model="apiServerInput"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="block text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
|
v-if="apiServerInput != apiServer"
|
||||||
@click="submitFile()"
|
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||||
|
@click="onClickSaveApiServer()"
|
||||||
>
|
>
|
||||||
Import Settings & Contacts
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||||
<br />
|
</button>
|
||||||
(excluding Identifier Data)
|
<button
|
||||||
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
|
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
||||||
|
>
|
||||||
|
Use Prod
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
|
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
||||||
|
>
|
||||||
|
Use Test
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||||
|
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
||||||
|
>
|
||||||
|
Use Local
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label
|
||||||
|
for="toggleProdWarningMessage"
|
||||||
|
class="flex items-center justify-between cursor-pointer px-4 py-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 px-4 py-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>
|
</div>
|
||||||
|
|
||||||
<div class="flex py-2">
|
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||||
<button>
|
Notification Push Server
|
||||||
<router-link
|
</h2>
|
||||||
:to="{ name: 'statistics' }"
|
<div class="px-3 py-4">
|
||||||
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
|
||||||
>
|
|
||||||
See Global Animated History of Giving
|
|
||||||
</router-link>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
|
||||||
<router-link
|
|
||||||
id="switch-identity-link"
|
|
||||||
:to="{ name: 'identity-switcher' }"
|
|
||||||
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
|
||||||
>
|
|
||||||
Switch Identity
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<div class="flex py-4">
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
|
||||||
v-model="apiServerInput"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-if="apiServerInput != apiServer"
|
|
||||||
class="px-4 rounded bg-red-500 border border-slate-400"
|
|
||||||
@click="onClickSaveApiServer()"
|
|
||||||
>
|
|
||||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
||||||
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
|
||||||
>
|
|
||||||
Use Prod
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
||||||
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
|
||||||
>
|
|
||||||
Use Test
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
||||||
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
|
||||||
>
|
|
||||||
Use Local
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label
|
|
||||||
for="toggleProdWarningMessage"
|
|
||||||
class="flex items-center justify-between cursor-pointer my-4"
|
|
||||||
@click="toggleProdWarning"
|
|
||||||
>
|
|
||||||
<!-- label -->
|
|
||||||
<h2>Show warning if on prod server</h2>
|
|
||||||
<!-- toggle -->
|
|
||||||
<div class="relative ml-2">
|
|
||||||
<!-- input -->
|
|
||||||
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
|
|
||||||
<!-- line -->
|
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
|
||||||
<!-- dot -->
|
|
||||||
<div
|
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
for="toggleTestWarningMessage"
|
|
||||||
class="flex items-center justify-between cursor-pointer my-4"
|
|
||||||
@click="toggleTestWarning"
|
|
||||||
>
|
|
||||||
<!-- label -->
|
|
||||||
<h2>Show warning if on non-prod server</h2>
|
|
||||||
<!-- toggle -->
|
|
||||||
<div class="relative ml-2">
|
|
||||||
<!-- input -->
|
|
||||||
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
|
|
||||||
<!-- line -->
|
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
|
||||||
<!-- dot -->
|
|
||||||
<div
|
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex py-4">
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
|
||||||
Notification Push Server
|
|
||||||
</h2>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||||
@@ -417,7 +409,7 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="webPushServerInput != webPushServer"
|
v-if="webPushServerInput != webPushServer"
|
||||||
class="px-4 rounded bg-red-500 border border-slate-400"
|
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||||
@click="onClickSavePushServer()"
|
@click="onClickSavePushServer()"
|
||||||
>
|
>
|
||||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||||
@@ -444,8 +436,40 @@
|
|||||||
<span class="px-4 text-sm" v-if="!webPushServerInput">
|
<span class="px-4 text-sm" v-if="!webPushServerInput">
|
||||||
When that setting is blank, this app will use the default web push
|
When that setting is blank, this app will use the default web push
|
||||||
server URL:
|
server URL:
|
||||||
{{ AppConstants.DEFAULT_PUSH_SERVER }}
|
{{ DEFAULT_PUSH_SERVER }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h2 class="text-slate-500 text-sm font-bold">
|
||||||
|
Contacts & Settings Database
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="ml-4 mt-2">
|
||||||
|
Import
|
||||||
|
<input type="file" @change="uploadFile" class="ml-2" />
|
||||||
|
<div v-if="showContactImport()">
|
||||||
|
<button
|
||||||
|
class="block text-center text-md uppercase bg-blue-500 text-white px-1.5 py-2 rounded-md mb-6"
|
||||||
|
@click="submitFile()"
|
||||||
|
>
|
||||||
|
Import Settings & Contacts
|
||||||
|
<br />
|
||||||
|
(excluding Identifier Data)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-4">
|
||||||
|
<button>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'statistics' }"
|
||||||
|
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
See Global Animated History of Giving
|
||||||
|
</router-link>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -454,19 +478,19 @@
|
|||||||
import { AxiosError, AxiosRequestConfig } from "axios";
|
import { AxiosError, AxiosRequestConfig } from "axios";
|
||||||
import Dexie from "dexie";
|
import Dexie from "dexie";
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
|
import { ImportProgress } from "dexie-export-import/dist/import";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString, DEFAULT_PUSH_SERVER } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
||||||
import { ImportProgress } from "dexie-export-import/dist/import";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
@@ -492,6 +516,7 @@ export default class AccountViewView extends Vue {
|
|||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
AppConstants = AppString;
|
AppConstants = AppString;
|
||||||
|
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
@@ -502,32 +527,23 @@ export default class AccountViewView extends Vue {
|
|||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
isSubscribed = false;
|
isSubscribed = false;
|
||||||
notificationMaybeChanged = false;
|
notificationMaybeChanged = false;
|
||||||
numAccounts = 0;
|
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
publicBase64 = "";
|
publicBase64 = "";
|
||||||
webPushServer = "";
|
webPushServer = "";
|
||||||
webPushServerInput = "";
|
webPushServerInput = "";
|
||||||
limits: RateLimits | null = null;
|
limits: RateLimits | null = null;
|
||||||
limitsMessage = "";
|
limitsMessage = "";
|
||||||
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
|
loadingLimits = false;
|
||||||
showContactGives = false;
|
showContactGives = false;
|
||||||
|
|
||||||
showDidCopy = false;
|
showDidCopy = false;
|
||||||
showDerCopy = false;
|
showDerCopy = false;
|
||||||
showB64Copy = false;
|
showB64Copy = false;
|
||||||
showPubCopy = false;
|
showPubCopy = false;
|
||||||
|
|
||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
|
|
||||||
subscription: PushSubscription | null = null;
|
subscription: PushSubscription | null = null;
|
||||||
warnIfProdServer = false;
|
warnIfProdServer = false;
|
||||||
warnIfTestServer = false;
|
warnIfTestServer = false;
|
||||||
|
|
||||||
async beforeCreate() {
|
|
||||||
await accountsDB.open();
|
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Async function executed when the component is created.
|
* Async function executed when the component is created.
|
||||||
* Initializes the component's state with values from the database,
|
* Initializes the component's state with values from the database,
|
||||||
@@ -672,7 +688,7 @@ export default class AccountViewView extends Vue {
|
|||||||
) {
|
) {
|
||||||
this.publicHex = identity.keys[0].publicKeyHex;
|
this.publicHex = identity.keys[0].publicKeyHex;
|
||||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||||
this.derivationPath = identity.keys[0].meta.derivationPath as string;
|
this.derivationPath = identity.keys[0].meta?.derivationPath as string;
|
||||||
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
activeDid: identity.did,
|
activeDid: identity.did,
|
||||||
@@ -716,21 +732,20 @@ export default class AccountViewView extends Vue {
|
|||||||
if (
|
if (
|
||||||
err instanceof Error &&
|
err instanceof Error &&
|
||||||
err.message ===
|
err.message ===
|
||||||
"Attempted to load account records with no identity available."
|
"Attempted to load account records with no identifier available."
|
||||||
) {
|
) {
|
||||||
this.limitsMessage = "No identity.";
|
this.limitsMessage = "No identifier.";
|
||||||
this.loadingLimits = false;
|
|
||||||
} else {
|
} else {
|
||||||
|
console.error("Telling user to clear cache at page create because:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Creating Account",
|
title: "Error Loading Account",
|
||||||
text: "Clear your cache and start over (after data backup).",
|
text: "Clear your cache and start over (after data backup).",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error("Telling user to clear cache at page create because:", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,6 +954,18 @@ export default class AccountViewView extends Vue {
|
|||||||
console.log(
|
console.log(
|
||||||
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
||||||
);
|
);
|
||||||
|
if (progress.done) {
|
||||||
|
console.log(`Imported ${progress.completedTables} tables.`);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Import Complete",
|
||||||
|
text: "",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,6 +973,9 @@ export default class AccountViewView extends Vue {
|
|||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
if (identity) {
|
if (identity) {
|
||||||
this.checkLimitsFor(identity);
|
this.checkLimitsFor(identity);
|
||||||
|
} else {
|
||||||
|
this.limitsMessage =
|
||||||
|
"You have no identifier, or your data has been corrupted.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,6 +1016,17 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleRateLimitsError(error);
|
this.handleRateLimitsError(error);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
isRegistered: false,
|
||||||
|
});
|
||||||
|
this.isRegistered = false;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Got an error marking user not registered:", err);
|
||||||
|
// already set an error notification for the user
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingLimits = false;
|
this.loadingLimits = false;
|
||||||
@@ -1014,17 +1055,12 @@ export default class AccountViewView extends Vue {
|
|||||||
this.limitsMessage =
|
this.limitsMessage =
|
||||||
(data?.error?.message as string) || "Bad server response.";
|
(data?.error?.message as string) || "Bad server response.";
|
||||||
console.error(
|
console.error(
|
||||||
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
|
"Got bad response retrieving limits, which usually means user isn't registered:",
|
||||||
this.limitsMessage,
|
error,
|
||||||
);
|
);
|
||||||
} else if (
|
|
||||||
error instanceof Error &&
|
|
||||||
error.message ===
|
|
||||||
"Attempted to load Give records with no identity available."
|
|
||||||
) {
|
|
||||||
this.limitsMessage = "No identity.";
|
|
||||||
} else {
|
} else {
|
||||||
// Handle other unknown errors
|
this.limitsMessage = "Got an error retrieving limits.";
|
||||||
|
console.error("Got some error retrieving limits:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,33 +18,137 @@
|
|||||||
|
|
||||||
<!-- Details -->
|
<!-- Details -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<div>
|
<div class="block flex gap-4 overflow-hidden">
|
||||||
<div class="block flex gap-4 overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<div class="overflow-hidden">
|
<h2 class="text-md font-bold">
|
||||||
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
|
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
|
||||||
<div class="text-sm">
|
</h2>
|
||||||
<div>
|
<div class="text-sm">
|
||||||
{{ veriClaim.claimType }}
|
<div>
|
||||||
</div>
|
{{ veriClaim.id }}
|
||||||
<div>
|
<button
|
||||||
<fa icon="message" class="fa-fw text-slate-400"></fa>
|
@click="
|
||||||
{{ veriClaim.claim?.description }}
|
libsUtil.doCopyTwoSecRedo(
|
||||||
</div>
|
veriClaim.id as string,
|
||||||
<div>
|
() => (showIdCopy = !showIdCopy),
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
)
|
||||||
{{ veriClaim.issuer }}
|
"
|
||||||
</div>
|
class="ml-2 mr-2"
|
||||||
<div>
|
>
|
||||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
|
</button>
|
||||||
</div>
|
<span v-show="showIdCopy">Copied ID</span>
|
||||||
|
</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 }}
|
||||||
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(veriClaim.issuer)">
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
libsUtil.doCopyTwoSecRedo(
|
||||||
|
veriClaim.issuer as string,
|
||||||
|
() => (showDidCopy = !showDidCopy),
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="ml-2 mr-2"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
<span v-show="showDidCopy">Copied DID</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fullfills Links -->
|
||||||
|
|
||||||
|
<!-- fullfills links for a give -->
|
||||||
|
<div v-if="detailsForGive?.fulfillsPlanHandleId">
|
||||||
|
<router-link
|
||||||
|
:to="
|
||||||
|
'/project/' +
|
||||||
|
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
||||||
|
"
|
||||||
|
class="text-blue-500 mt-2"
|
||||||
|
>
|
||||||
|
Fulfills a bigger plan...
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<!-- if there's another, it's probably fulfilling an offer, too -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
detailsForGive?.fulfillsType &&
|
||||||
|
detailsForGive?.fulfillsType !== 'PlanAction' &&
|
||||||
|
detailsForGive?.fulfillsHandleId
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- router-link to /claim/ only changes URL path -->
|
||||||
|
<a
|
||||||
|
@click="
|
||||||
|
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
|
||||||
|
"
|
||||||
|
class="text-blue-500 mt-4"
|
||||||
|
>
|
||||||
|
Fulfills
|
||||||
|
{{
|
||||||
|
capitalizeAndInsertSpacesBeforeCaps(
|
||||||
|
detailsForGive.fulfillsType,
|
||||||
|
)
|
||||||
|
}}...
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- fullfills links for an offer -->
|
||||||
|
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
||||||
|
<router-link
|
||||||
|
:to="
|
||||||
|
'/project/' +
|
||||||
|
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
||||||
|
"
|
||||||
|
class="text-blue-500 mt-4"
|
||||||
|
>
|
||||||
|
Offered to a bigger plan...
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="columns-3">
|
||||||
|
<button
|
||||||
|
class="col-span-1 bg-blue-600 text-white px-4 py-2 rounded-md"
|
||||||
|
v-if="
|
||||||
|
libsUtil.isGiveRecordTheUserCanConfirm(
|
||||||
|
veriClaim,
|
||||||
|
activeDid,
|
||||||
|
confirmerIdList,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="confirmClaim()"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="libsUtil.canFulfillOffer(veriClaim)"
|
||||||
|
@click="openFulfillGiftDialog()"
|
||||||
|
class="col-span-1 block w-fit text-center text-md bg-blue-600 text-white px-1.5 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Affirm Delivery
|
||||||
|
<fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<GiftedDialog ref="customGiveDialog" message="Offer fulfilled by" />
|
||||||
|
|
||||||
|
<div v-if="libsUtil.giveIsConfirmable(veriClaim)">
|
||||||
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
|
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
|
||||||
|
|
||||||
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
|
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
|
||||||
@@ -71,16 +175,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="confirmerIdList.length > 0">
|
<div v-if="confirmerIdList.length > 0">
|
||||||
The following people have issued or confirmed this claim.
|
The following people have issued or confirmed this claim.
|
||||||
<ul>
|
<ul class="ml-4">
|
||||||
<li
|
<li
|
||||||
v-for="confirmerId in confirmerIdList"
|
v-for="confirmerId in confirmerIdList"
|
||||||
:key="confirmerId"
|
:key="confirmerId"
|
||||||
class="list-disc"
|
class="list-disc ml-4"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ confirmerId }}
|
{{ didInfo(confirmerId) }}
|
||||||
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
copyToClipboard(
|
||||||
|
'The DID of ' + confirmerId,
|
||||||
|
confirmerId,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,25 +205,39 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Never need to show the following message.
|
Never need to show this message:
|
||||||
|
"Nobody that you know can see someone who has confirmed this claim."
|
||||||
|
|
||||||
If there is nobody in the confirmerIdList then we'll show the combined "nobody" 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.
|
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. -->
|
|
||||||
|
|
||||||
|
<!-- Now show anyone linked to confirmers. -->
|
||||||
<div v-if="confsVisibleToIdList.length > 0">
|
<div v-if="confsVisibleToIdList.length > 0">
|
||||||
The following people can connect you with people who have issued or
|
The following people can connect you with people who have issued or
|
||||||
confirmed this claim.
|
confirmed this claim.
|
||||||
<ul>
|
<ul class="ml-4">
|
||||||
<li
|
<li
|
||||||
v-for="confsVisibleTo in confsVisibleToIdList"
|
v-for="confsVisibleTo in confsVisibleToIdList"
|
||||||
:key="confsVisibleTo"
|
:key="confsVisibleTo"
|
||||||
class="list-disc"
|
class="list-disc ml-4"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ confsVisibleTo }}
|
{{ didInfo(confsVisibleTo) }}
|
||||||
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
copyToClipboard(
|
||||||
|
'The DID of ' + confsVisibleTo,
|
||||||
|
confsVisibleTo,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,27 +246,120 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<!-- explain if user cannot confirm -->
|
||||||
<div v-if="confirmerIdList.includes(activeDid)">
|
<!-- Note that these conditions are mirrored in userCanConfirm(). -->
|
||||||
You have confirmed this claim.
|
<div v-if="confirmerIdList.includes(activeDid)">
|
||||||
</div>
|
You have confirmed this claim.
|
||||||
<div v-else-if="containsHiddenDid(veriClaim.claim)">
|
</div>
|
||||||
You cannot confirm this claim because it contains data that is hidden
|
<div v-else-if="veriClaim.issuer == activeDid">
|
||||||
from you.
|
You cannot confirm this because you issued this claim, so you already
|
||||||
</div>
|
count as confirming it.
|
||||||
<div v-else>
|
</div>
|
||||||
<button
|
<div v-else-if="serverUtil.containsHiddenDid(veriClaim.claim)">
|
||||||
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
|
You cannot confirm this because it contains hidden identifiers.
|
||||||
@click="confirmClaim(veriClaim.id)"
|
|
||||||
>
|
|
||||||
Confirm Claim
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2>
|
<h2 class="font-bold uppercase text-xl mt-8 mb-2">
|
||||||
|
{{ serverUtil.containsHiddenDid(veriClaim) ? "Visible " : "" }}Details
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
serverUtil.containsHiddenDid(veriClaim) &&
|
||||||
|
R.isEmpty(veriClaimDidsVisible)
|
||||||
|
"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
Some of the details are not visible to you; they show as "HIDDEN". They
|
||||||
|
are not visible to any of your direct contacts, either.
|
||||||
|
<span v-if="canShare">
|
||||||
|
If you'd like to ask any of your contacts to take a look and see if
|
||||||
|
their contacts can see more details,
|
||||||
|
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||||
|
>click to send them this info</a
|
||||||
|
>
|
||||||
|
and see if they are willing to make an introduction.
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
If you'd like to ask any of your contacts to take a look and see if
|
||||||
|
their contacts can see more details,
|
||||||
|
<a
|
||||||
|
@click="copyToClipboard('Location', windowLocation)"
|
||||||
|
class="text-blue-500"
|
||||||
|
>share this page with them</a
|
||||||
|
>
|
||||||
|
and see if they are willing to make an introduction.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!R.isEmpty(veriClaimDidsVisible)">
|
||||||
|
Some of the details are not visible to you but they are visible to some
|
||||||
|
of your contacts.
|
||||||
|
<span v-if="canShare">
|
||||||
|
If you'd like an introduction,
|
||||||
|
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||||
|
>click to share the information with them and ask if they'll tell
|
||||||
|
you more about the participants.</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
If you'd like an introduction,
|
||||||
|
<a
|
||||||
|
@click="copyToClipboard('Location', windowLocation)"
|
||||||
|
class="text-blue-500"
|
||||||
|
>share this page with them and ask if they'll tell you more about
|
||||||
|
about the participants.</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(visibleDidPath, index) of Object.keys(veriClaimDidsVisible)"
|
||||||
|
:key="index"
|
||||||
|
class="list-disc p-4"
|
||||||
|
>
|
||||||
|
<div class="text-sm">
|
||||||
|
<fa icon="minus" class="fa-fw"></fa>
|
||||||
|
The {{ visibleDidPath }} is visible to:
|
||||||
|
</div>
|
||||||
|
<div class="ml-12 p-1">
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
v-for="(visDid, idx2) of veriClaimDidsVisible[visibleDidPath]"
|
||||||
|
:key="idx2"
|
||||||
|
class="list-disc"
|
||||||
|
>
|
||||||
|
<div class="text-sm mt-2">
|
||||||
|
<span>
|
||||||
|
{{ didInfo(visDid) }}
|
||||||
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
|
<button
|
||||||
|
@click="copyToClipboard('The DID of ' + visDid, visDid)"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span v-if="veriClaim.publicUrls?.[visDid]"
|
||||||
|
>, found at
|
||||||
|
<fa icon="globe" class="fa-fw text-slate-400"></fa
|
||||||
|
> <a
|
||||||
|
:href="veriClaim.publicUrls?.[visDid]"
|
||||||
|
class="text-blue-500"
|
||||||
|
>{{
|
||||||
|
veriClaim.publicUrls[visDid].substring(
|
||||||
|
veriClaim.publicUrls[visDid].indexOf("//") + 2,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
||||||
<pre
|
<pre
|
||||||
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
|
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
|
||||||
>{{ veriClaimDump }}</pre
|
>{{ veriClaimDump }}</pre
|
||||||
@@ -155,7 +378,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else
|
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"
|
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
@click="showFullClaim(veriClaim.id)"
|
@click="showFullClaim(veriClaim.id as string)"
|
||||||
>
|
>
|
||||||
Load Full Claim Details
|
Load Full Claim Details
|
||||||
</button>
|
</button>
|
||||||
@@ -179,8 +402,8 @@ import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
|||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import * as util from "util";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import OfferDialog from "@/components/OfferDialog.vue";
|
import OfferDialog from "@/components/OfferDialog.vue";
|
||||||
@@ -189,9 +412,11 @@ 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 * as serverUtil from "@/libs/endorserServer";
|
import * as serverUtil from "@/libs/endorserServer";
|
||||||
|
import * as libsUtil from "@/libs/util";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { GiverInputInfo } from "@/libs/endorserServer";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -206,23 +431,47 @@ interface Notification {
|
|||||||
export default class ClaimView extends Vue {
|
export default class ClaimView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
accountIdentityStr: string = "null";
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
confirmerIdList = []; // list of DIDs that have confirmed this claim excluding the issuer
|
|
||||||
|
canShare = false;
|
||||||
|
confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
|
||||||
confsVisibleErrorMessage = "";
|
confsVisibleErrorMessage = "";
|
||||||
confsVisibleToIdList = []; // list of DIDs that can see any confirmer
|
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
||||||
|
detailsForGive = null;
|
||||||
|
detailsForOffer = null;
|
||||||
fullClaim = null;
|
fullClaim = null;
|
||||||
fullClaimDump = "";
|
fullClaimDump = "";
|
||||||
fullClaimMessage = "";
|
fullClaimMessage = "";
|
||||||
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
|
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
|
||||||
|
showDidCopy = false;
|
||||||
|
showIdCopy = false;
|
||||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
veriClaimDump = "";
|
veriClaimDump = "";
|
||||||
|
veriClaimDidsVisible = {};
|
||||||
|
windowLocation = window.location.href;
|
||||||
|
|
||||||
util = util;
|
R = R;
|
||||||
yaml = yaml;
|
yaml = yaml;
|
||||||
containsHiddenDid = serverUtil.containsHiddenDid;
|
libsUtil = libsUtil;
|
||||||
|
serverUtil = serverUtil;
|
||||||
|
|
||||||
|
resetThisValues() {
|
||||||
|
this.confirmerIdList = [];
|
||||||
|
this.confsVisibleErrorMessage = "";
|
||||||
|
this.confsVisibleToIdList = [];
|
||||||
|
this.detailsForGive = null;
|
||||||
|
this.detailsForOffer = null;
|
||||||
|
this.fullClaim = null;
|
||||||
|
this.fullClaimDump = "";
|
||||||
|
this.fullClaimMessage = "";
|
||||||
|
this.numConfsNotVisible = 0;
|
||||||
|
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
|
this.veriClaimDump = "";
|
||||||
|
}
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -236,13 +485,14 @@ export default class ClaimView extends Vue {
|
|||||||
const accountsArr = await accounts?.toArray();
|
const accountsArr = await accounts?.toArray();
|
||||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
this.accountIdentityStr = account?.identity || "null";
|
||||||
|
const identity = JSON.parse(this.accountIdentityStr);
|
||||||
|
|
||||||
const pathParam = window.location.pathname.substring("/claim/".length);
|
const pathParam = window.location.pathname.substring("/claim/".length);
|
||||||
let claimId;
|
let claimId;
|
||||||
if (pathParam) {
|
if (pathParam) {
|
||||||
claimId = decodeURIComponent(pathParam);
|
claimId = decodeURIComponent(pathParam);
|
||||||
this.loadClaim(claimId, identity);
|
await this.loadClaim(claimId, identity);
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -254,6 +504,18 @@ export default class ClaimView extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
|
||||||
|
// then use this truer check: navigator.canShare && navigator.canShare()
|
||||||
|
this.canShare = !!navigator.share;
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert a space before any capital letters except the initial letter
|
||||||
|
// (and capitalize initial letter, just in case)
|
||||||
|
capitalizeAndInsertSpacesBeforeCaps(text: string) {
|
||||||
|
return !text
|
||||||
|
? ""
|
||||||
|
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||||
}
|
}
|
||||||
|
|
||||||
totalConfirmers() {
|
totalConfirmers() {
|
||||||
@@ -274,7 +536,7 @@ export default class ClaimView extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load project records with no identity available.",
|
"Attempted to load project records with no identifier available.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
@@ -292,18 +554,20 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Isn't there a better way to make this available to the template?
|
// Isn't there a better way to make this available to the template?
|
||||||
didInfo(
|
didInfo(did: string) {
|
||||||
did: string,
|
return serverUtil.didInfo(
|
||||||
activeDid: string,
|
did,
|
||||||
dids: Array<string>,
|
this.activeDid,
|
||||||
contacts: Array<Contact>,
|
this.allMyDids,
|
||||||
) {
|
this.allContacts,
|
||||||
return serverUtil.didInfo(did, activeDid, dids, contacts);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadClaim(claimId: string, identity: IIdentifier) {
|
async loadClaim(claimId: string, identity: IIdentifier) {
|
||||||
const url =
|
const urlPath = libsUtil.isGlobalUri(claimId)
|
||||||
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(claimId);
|
? "/api/claim/byHandle/"
|
||||||
|
: "/api/claim/";
|
||||||
|
const url = this.apiServer + urlPath + encodeURIComponent(claimId);
|
||||||
const headers = await this.getHeaders(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -311,39 +575,62 @@ export default class ClaimView extends Vue {
|
|||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.veriClaim = resp.data;
|
this.veriClaim = resp.data;
|
||||||
this.veriClaimDump = yaml.dump(this.veriClaim);
|
this.veriClaimDump = yaml.dump(this.veriClaim);
|
||||||
|
this.veriClaimDidsVisible = libsUtil.findAllVisibleToDids(
|
||||||
|
this.veriClaim,
|
||||||
|
true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// actually, axios typically throws an error so we never get here
|
// actually, axios typically throws an error so we never get here
|
||||||
console.log("Error getting claim:", resp);
|
console.error("Error getting claim:", resp);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was a problem getting that claim. See logs for more info.",
|
text: "There was a problem retrieving that claim.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} 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 =
|
// retrieve more details on Give, Offer, or Plan
|
||||||
this.apiServer +
|
if (this.veriClaim.claimType === "GiveAction") {
|
||||||
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
|
const giveUrl =
|
||||||
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
|
this.apiServer +
|
||||||
const confirmHeaders = await this.getHeaders(identity);
|
"/api/v2/report/gives?handleId=" +
|
||||||
try {
|
encodeURIComponent(this.veriClaim.handleId as string);
|
||||||
|
const giveHeaders = await this.getHeaders(identity);
|
||||||
|
const giveResp = await this.axios.get(giveUrl, {
|
||||||
|
headers: giveHeaders,
|
||||||
|
});
|
||||||
|
if (giveResp.status === 200) {
|
||||||
|
this.detailsForGive = giveResp.data.data[0];
|
||||||
|
} else {
|
||||||
|
console.error("Error getting detailed give info:", giveResp);
|
||||||
|
}
|
||||||
|
} else if (this.veriClaim.claimType === "Offer") {
|
||||||
|
const offerUrl =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/offers?handleId=" +
|
||||||
|
encodeURIComponent(this.veriClaim.handleId as string);
|
||||||
|
const offerHeaders = await this.getHeaders(identity);
|
||||||
|
const offerResp = await this.axios.get(offerUrl, {
|
||||||
|
headers: offerHeaders,
|
||||||
|
});
|
||||||
|
if (offerResp.status === 200) {
|
||||||
|
this.detailsForOffer = offerResp.data.data[0];
|
||||||
|
} else {
|
||||||
|
console.error("Error getting detailed offer info:", offerResp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve the list of confirmers
|
||||||
|
const confirmUrl =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
|
||||||
|
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
|
||||||
|
const confirmHeaders = await this.getHeaders(identity);
|
||||||
const response = await this.axios.get(confirmUrl, {
|
const response = await this.axios.get(confirmUrl, {
|
||||||
headers: confirmHeaders,
|
headers: confirmHeaders,
|
||||||
});
|
});
|
||||||
@@ -369,16 +656,23 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
console.error("Error retrieving confirmations:", serverError);
|
console.error("Error retrieving claim:", serverError);
|
||||||
this.confsVisibleErrorMessage =
|
this.$notify(
|
||||||
"Had problems retrieving confirmations. See logs for more info.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving claim data.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async showFullClaim(claimId: string) {
|
async showFullClaim(claimId: string) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = accountsDB.accounts;
|
const accounts = accountsDB.accounts;
|
||||||
const accountsArr = await accounts?.toArray();
|
const accountsArr: Account[] = await accounts?.toArray();
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
@@ -393,7 +687,7 @@ export default class ClaimView extends Vue {
|
|||||||
this.fullClaimDump = yaml.dump(this.fullClaim);
|
this.fullClaimDump = yaml.dump(this.fullClaim);
|
||||||
} else {
|
} else {
|
||||||
// actually, axios typically throws an error so we never get here
|
// actually, axios typically throws an error so we never get here
|
||||||
console.log("Error getting full claim:", resp);
|
console.error("Error getting full claim:", resp);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -429,6 +723,7 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// similar code is found in ProjectViewView
|
||||||
async confirmClaim() {
|
async confirmClaim() {
|
||||||
if (confirm("Do you personally confirm that this is true?")) {
|
if (confirm("Do you personally confirm that this is true?")) {
|
||||||
// similar logic is found in endorser-mobile
|
// similar logic is found in endorser-mobile
|
||||||
@@ -466,7 +761,7 @@ export default class ClaimView extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log("Got error submitting the confirmation:", result);
|
console.error("Got error submitting the confirmation:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -479,5 +774,49 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showDifferentClaimPage(claimId: string) {
|
||||||
|
const route = {
|
||||||
|
path: "/claim/" + encodeURIComponent(claimId),
|
||||||
|
};
|
||||||
|
this.$router.push(route).then(async () => {
|
||||||
|
this.resetThisValues();
|
||||||
|
await this.loadClaim(claimId, JSON.parse(this.accountIdentityStr));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openFulfillGiftDialog() {
|
||||||
|
const giver: GiverInputInfo = {
|
||||||
|
did: libsUtil.offerGiverDid(this.veriClaim),
|
||||||
|
};
|
||||||
|
(this.$refs.customGiveDialog as GiftedDialog).open(
|
||||||
|
giver,
|
||||||
|
this.veriClaim.handleId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyToClipboard(name: string, text: string) {
|
||||||
|
useClipboard()
|
||||||
|
.copy(text)
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Copied",
|
||||||
|
text: (name || "That") + " was copied to the clipboard.",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickShareClaim() {
|
||||||
|
window.navigator.share({
|
||||||
|
title: "Help Connect Me",
|
||||||
|
text: "I'm trying to find the full details of this claim. Can you help me?",
|
||||||
|
url: this.windowLocation,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||||
Given with {{ contact?.name }}
|
Transferred with {{ contact?.name }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load Give records with no identity available.",
|
"Attempted to load Give records with no identifier available.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
@@ -193,7 +193,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 your settings or contacts or gives.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,13 +20,13 @@
|
|||||||
<ul class="border-t border-slate-300">
|
<ul class="border-t border-slate-300">
|
||||||
<li class="border-b border-slate-300 py-3">
|
<li class="border-b border-slate-300 py-3">
|
||||||
<h2 class="text-base flex gap-4 items-center">
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
<span class="grow italic text-slate-500"
|
<span class="grow">
|
||||||
><EntityIcon
|
<img
|
||||||
:entityId="null"
|
src="../assets/blank-square.svg"
|
||||||
:iconSize="32"
|
width="32"
|
||||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||||
></EntityIcon>
|
/>
|
||||||
Anonymous
|
Anonymous/Unnamed
|
||||||
</span>
|
</span>
|
||||||
<span class="text-right">
|
<span class="text-right">
|
||||||
<button
|
<button
|
||||||
@@ -45,12 +45,12 @@
|
|||||||
class="border-b border-slate-300 py-3"
|
class="border-b border-slate-300 py-3"
|
||||||
>
|
>
|
||||||
<h2 class="text-base flex gap-4 items-center">
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
<span class="grow font-semibold"
|
<span class="grow font-semibold">
|
||||||
><EntityIcon
|
<EntityIcon
|
||||||
:entityId="contact.did"
|
:entityId="contact.did"
|
||||||
:iconSize="32"
|
:iconSize="32"
|
||||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||||
></EntityIcon>
|
/>
|
||||||
{{ contact.name || "(no name)" }}
|
{{ contact.name || "(no name)" }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-right">
|
<span class="text-right">
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<GiftedDialog
|
<GiftedDialog
|
||||||
ref="customDialog"
|
ref="customDialog"
|
||||||
message="Received from"
|
message="Received from"
|
||||||
showGivenToUser="true"
|
:projectId="projectId"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,12 +105,40 @@ export default class ContactGiftingView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
accounts: typeof AccountsSchema;
|
accounts: typeof AccountsSchema;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
projectId = localStorage.getItem("projectId") || "";
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
accountsDB.open();
|
accountsDB.open();
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
|
localStorage.removeItem("projectId");
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log("Error retrieving settings & contacts:", err);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
err.message ||
|
||||||
|
"There was an error retrieving your settings or contacts.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const account = (await accountsDB.accounts
|
const account = (await accountsDB.accounts
|
||||||
@@ -121,7 +149,7 @@ export default class ContactGiftingView extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load Give records with no identity available.",
|
"Attempted to load Give records with no identifier available.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
@@ -136,30 +164,6 @@ export default class ContactGiftingView extends Vue {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
|
||||||
this.apiServer = settings?.apiServer || "";
|
|
||||||
this.activeDid = settings?.activeDid || "";
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (err: any) {
|
|
||||||
console.log("Error retrieving settings & contacts:", err);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text:
|
|
||||||
err.message ||
|
|
||||||
"There was an error retrieving your settings and/or contacts.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openDialog(giver: GiverInputInfo) {
|
openDialog(giver: GiverInputInfo) {
|
||||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,19 +41,30 @@
|
|||||||
:dotsOptions="{ type: 'square' }"
|
:dotsOptions="{ type: 'square' }"
|
||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
/>
|
/>
|
||||||
|
<span class="flex justify-center">
|
||||||
|
Click QR to copy your contact URL to your clipboard.
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center" v-else>
|
<div class="text-center" v-else>
|
||||||
You have no identitifiers yet, so
|
You have no identitifiers yet, so
|
||||||
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
<router-link
|
||||||
|
:to="{ name: 'start' }"
|
||||||
|
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
||||||
|
>
|
||||||
create your identifier.
|
create your identifier.
|
||||||
</router-link>
|
</router-link>
|
||||||
<br />
|
<br />
|
||||||
We recommend you do that first; otherwise, these contacts won't see your
|
If you don't that first, these contacts won't see your activity.
|
||||||
activity.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
|
<div class="text-center">
|
||||||
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
<h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1>
|
||||||
|
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
||||||
|
<span>
|
||||||
|
If you do not see a scanning camera window here, check your camera
|
||||||
|
permissions.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -112,7 +123,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to show contact info with no identity available.",
|
"Attempted to show contact info with no identifier available.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
|
|||||||
@@ -9,27 +9,27 @@
|
|||||||
<div class="flex justify-between py-2">
|
<div class="flex justify-between py-2">
|
||||||
<span />
|
<span />
|
||||||
<span>
|
<span>
|
||||||
<router-link
|
<a
|
||||||
:to="{ name: 'help' }"
|
@click="showHintsForOnboarding()"
|
||||||
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
|
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
|
||||||
>
|
>
|
||||||
Help
|
Onboarding Guide
|
||||||
</router-link>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Contact -->
|
<!-- New Contact -->
|
||||||
<div class="mb-4 flex items-stretch">
|
<div class="mt-4 mb-4 flex items-stretch">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contact-qr' }"
|
:to="{ name: 'contact-qr' }"
|
||||||
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
|
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
|
||||||
>
|
>
|
||||||
<fa icon="qrcode" class="fa-fw text-2xl" />
|
<fa icon="qrcode" class="fa-fw text-2xl" />
|
||||||
</router-link>
|
</router-link>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="DID, Name, Public Key, Next Public Key Hash"
|
placeholder="URL or DID, Name, Public Key, Next Public Key Hash"
|
||||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
|
||||||
v-model="contactInput"
|
v-model="contactInput"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -44,14 +44,14 @@
|
|||||||
<div class="w-full text-right">
|
<div class="w-full text-right">
|
||||||
Hours to Add:
|
Hours to Add:
|
||||||
<input
|
<input
|
||||||
class="border border rounded border-slate-400 w-24 text-right"
|
class="border rounded border-slate-400 w-24 text-right"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
v-model="hourInput"
|
v-model="hourInput"
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<input
|
<input
|
||||||
class="border border rounded border-slate-400 w-48"
|
class="border rounded border-slate-400 w-48"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
v-model="hourDescriptionInput"
|
v-model="hourDescriptionInput"
|
||||||
@@ -73,9 +73,13 @@
|
|||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
<br />
|
<br />
|
||||||
(Only hours shown)
|
(Only most recent hours included. To see more, click
|
||||||
<br />
|
<span
|
||||||
(Only recent shown)
|
class="text-sm uppercase bg-slate-500 text-white px-1 py-1 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="file-lines" class="fa-fw" />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -92,8 +96,9 @@
|
|||||||
:entityId="contact.did"
|
:entityId="contact.did"
|
||||||
:iconSize="24"
|
:iconSize="24"
|
||||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||||
|
@click="showLargeIdenticon = contact.did"
|
||||||
></EntityIcon>
|
></EntityIcon>
|
||||||
{{ contact.name || "(no name)" }}
|
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||||
<button
|
<button
|
||||||
class="text-sm uppercase bg-slate-500 text-white px-1 rounded-md"
|
class="text-sm uppercase bg-slate-500 text-white px-1 rounded-md"
|
||||||
@click="
|
@click="
|
||||||
@@ -105,7 +110,21 @@
|
|||||||
<fa icon="pen" class="fa-fw" />
|
<fa icon="pen" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-sm truncate">{{ contact.did }}</div>
|
<div class="text-sm truncate">
|
||||||
|
{{ contact.did }}
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
libsUtil.doCopyTwoSecRedo(
|
||||||
|
contact.did,
|
||||||
|
() => (showDidCopy = !showDidCopy),
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="ml-2 mr-2"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
<span v-show="showDidCopy">Copied DID</span>
|
||||||
|
</div>
|
||||||
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
||||||
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
||||||
</div>
|
</div>
|
||||||
@@ -144,19 +163,14 @@
|
|||||||
@click="register(contact)"
|
@click="register(contact)"
|
||||||
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
|
||||||
v-if="activeDid"
|
v-if="activeDid"
|
||||||
|
title="Registration"
|
||||||
>
|
>
|
||||||
<fa
|
<fa
|
||||||
v-if="contact.registered"
|
v-if="contact.registered"
|
||||||
icon="person-circle-check"
|
icon="person-circle-check"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
title="Registered"
|
|
||||||
/>
|
|
||||||
<fa
|
|
||||||
v-else
|
|
||||||
icon="person-circle-question"
|
|
||||||
class="fa-fw"
|
|
||||||
title="Registration Unknown"
|
|
||||||
/>
|
/>
|
||||||
|
<fa v-else icon="person-circle-question" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -216,7 +230,7 @@
|
|||||||
query: { contactDid: contact.did },
|
query: { contactDid: contact.did },
|
||||||
}"
|
}"
|
||||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
title="See all given activity"
|
title="See more given activity"
|
||||||
>
|
>
|
||||||
<fa icon="file-lines" class="fa-fw" />
|
<fa icon="file-lines" class="fa-fw" />
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -226,6 +240,18 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else>There are no contacts.</p>
|
<p v-else>There are no contacts.</p>
|
||||||
|
<div v-if="showLargeIdenticon" class="fixed z-[100] top-0 inset-x-0 w-full">
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="showLargeIdenticon"
|
||||||
|
:iconSize="512"
|
||||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
|
@click="showLargeIdenticon = ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="contactEdit !== null" class="dialog-overlay">
|
<div v-if="contactEdit !== null" class="dialog-overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
@@ -236,19 +262,21 @@
|
|||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
v-model="contactNewName"
|
v-model="contactNewName"
|
||||||
/>
|
/>
|
||||||
<button
|
<div class="flex justify-between">
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
<button
|
||||||
@click="onClickSaveName(contactEdit, contactNewName)"
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
||||||
>
|
@click="onClickSaveName(contactEdit, contactNewName)"
|
||||||
<fa icon="save" />
|
>
|
||||||
</button>
|
<fa icon="save" />
|
||||||
<span class="inline-block w-2" />
|
</button>
|
||||||
<button
|
<span class="inline-block w-2" />
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
<button
|
||||||
@click="onClickCancelName()"
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
||||||
>
|
@click="onClickCancelName()"
|
||||||
<fa icon="ban" />
|
>
|
||||||
</button>
|
<fa icon="ban" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -258,9 +286,10 @@
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
|
|
||||||
import { NotificationIface } from "@/constants/app";
|
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
import { AppString, NotificationIface } from "@/constants/app";
|
||||||
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";
|
||||||
@@ -270,16 +299,19 @@ import {
|
|||||||
SimpleSigner,
|
SimpleSigner,
|
||||||
} from "@/libs/crypto";
|
} from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
|
CONTACT_CSV_HEADER,
|
||||||
CONTACT_URL_PREFIX,
|
CONTACT_URL_PREFIX,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
GiveVerifiableCredential,
|
GiveVerifiableCredential,
|
||||||
|
isDid,
|
||||||
RegisterVerifiableCredential,
|
RegisterVerifiableCredential,
|
||||||
SERVICE_ID,
|
SERVICE_ID,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import * as libsUtil from "@/libs/util";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { IndexableType } from "dexie";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
@@ -312,9 +344,14 @@ export default class ContactsView extends Vue {
|
|||||||
hourDescriptionInput = "";
|
hourDescriptionInput = "";
|
||||||
hourInput = "0";
|
hourInput = "0";
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
|
showDidCopy = false;
|
||||||
showGiveNumbers = false;
|
showGiveNumbers = false;
|
||||||
showGiveTotals = true;
|
showGiveTotals = true;
|
||||||
showGiveConfirmed = true;
|
showGiveConfirmed = true;
|
||||||
|
showLargeIdenticon = "";
|
||||||
|
|
||||||
|
AppString = AppString;
|
||||||
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -334,7 +371,7 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (this.contactEndorserUrl) {
|
if (this.contactEndorserUrl) {
|
||||||
await this.newContactFromScan(this.contactEndorserUrl);
|
await this.addContactFromScan(this.contactEndorserUrl);
|
||||||
localStorage.removeItem("contactEndorserUrl");
|
localStorage.removeItem("contactEndorserUrl");
|
||||||
this.contactEndorserUrl = "";
|
this.contactEndorserUrl = "";
|
||||||
}
|
}
|
||||||
@@ -348,7 +385,7 @@ export default class ContactsView extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load Give records with no identity available.",
|
"Attempted to load Give records with no identifier available.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
@@ -477,6 +514,18 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showHintsForOnboarding() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Onboard Someone",
|
||||||
|
text: libsUtil.ONBOARD_MESSAGE,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async onClickNewContact(): Promise<void> {
|
async onClickNewContact(): Promise<void> {
|
||||||
if (!this.contactInput) {
|
if (!this.contactInput) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -492,7 +541,46 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
||||||
await this.newContactFromScan(this.contactInput);
|
await this.addContactFromScan(this.contactInput);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.contactInput.startsWith(CONTACT_CSV_HEADER)) {
|
||||||
|
const lines = this.contactInput.split(/\n/);
|
||||||
|
const lineAdded = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lineAdded.push(this.addContactFromEndorserMobileLine(line));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Promise.all(lineAdded);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Contacts Added",
|
||||||
|
text: "Each contact was added. Nothing was sent to the server.",
|
||||||
|
},
|
||||||
|
-1, // keeping it up so that the "visibility" message is seen
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Contacts Maybe Added",
|
||||||
|
text: "An error occurred. Some contacts may have been added.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const allContacts = await db.contacts.toArray();
|
||||||
|
this.contacts = R.sort(
|
||||||
|
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||||
|
allContacts,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,7 +621,48 @@ export default class ContactsView extends Vue {
|
|||||||
await this.addContact(newContact);
|
await this.addContact(newContact);
|
||||||
}
|
}
|
||||||
|
|
||||||
async newContactFromScan(url: string): Promise<void> {
|
async addContactFromEndorserMobileLine(line: string): Promise<IndexableType> {
|
||||||
|
// Note that Endorser Mobile puts name first, then did, etc.
|
||||||
|
let name = line;
|
||||||
|
let did = "";
|
||||||
|
let publicKeyInput, seesMe, registered;
|
||||||
|
const commaPos1 = line.indexOf(",");
|
||||||
|
if (commaPos1 > -1) {
|
||||||
|
name = line.substring(0, commaPos1).trim();
|
||||||
|
did = line.substring(commaPos1 + 1).trim();
|
||||||
|
const commaPos2 = line.indexOf(",", commaPos1 + 1);
|
||||||
|
if (commaPos2 > -1) {
|
||||||
|
did = line.substring(commaPos1 + 1, commaPos2).trim();
|
||||||
|
publicKeyInput = line.substring(commaPos2 + 1).trim();
|
||||||
|
const commaPos3 = line.indexOf(",", commaPos2 + 1);
|
||||||
|
if (commaPos3 > -1) {
|
||||||
|
publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
|
||||||
|
seesMe = line.substring(commaPos3 + 1).trim() == "true";
|
||||||
|
const commaPos4 = line.indexOf(",", commaPos3 + 1);
|
||||||
|
if (commaPos4 > -1) {
|
||||||
|
seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
|
||||||
|
registered = line.substring(commaPos4 + 1).trim() == "true";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// help with potential mistakes while this sharing requires copy-and-paste
|
||||||
|
let publicKeyBase64 = publicKeyInput;
|
||||||
|
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
||||||
|
// it must be all hex (compressed public key), so convert
|
||||||
|
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
||||||
|
}
|
||||||
|
const newContact = {
|
||||||
|
did,
|
||||||
|
name,
|
||||||
|
publicKeyBase64,
|
||||||
|
seesMe,
|
||||||
|
registered,
|
||||||
|
};
|
||||||
|
return db.contacts.add(newContact);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addContactFromScan(url: string): Promise<void> {
|
||||||
const payload = getContactPayloadFromJwtUrl(url);
|
const payload = getContactPayloadFromJwtUrl(url);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -569,6 +698,18 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isDid(newContact.did)) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid DID",
|
||||||
|
text: "The DID is not valid. It must begin with 'did:'",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
newContact.seesMe = true; // since we will immediately set that on the server
|
newContact.seesMe = true; // since we will immediately set that on the server
|
||||||
return db.contacts
|
return db.contacts
|
||||||
.add(newContact)
|
.add(newContact)
|
||||||
@@ -582,10 +723,9 @@ export default class ContactsView extends Vue {
|
|||||||
if (this.activeDid) {
|
if (this.activeDid) {
|
||||||
this.setVisibility(newContact, true, false);
|
this.setVisibility(newContact, true, false);
|
||||||
addedMessage =
|
addedMessage =
|
||||||
newContact.name +
|
"They were added, and your activity is visible to them.";
|
||||||
" was added, and your activity is visible to them.";
|
|
||||||
} else {
|
} else {
|
||||||
addedMessage = newContact.name + " was added.";
|
addedMessage = "They were added.";
|
||||||
}
|
}
|
||||||
if (this.isRegistered) {
|
if (this.isRegistered) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -593,10 +733,7 @@ export default class ContactsView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "New User?",
|
title: "New User?",
|
||||||
text:
|
text: "If they are a new user, be sure to register to onboard them.",
|
||||||
"If " +
|
|
||||||
newContact.name +
|
|
||||||
" is a new user, be sure to register them.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -620,6 +757,10 @@ export default class ContactsView extends Vue {
|
|||||||
message =
|
message =
|
||||||
"A contact with that DID is already in your contact list. Edit them directly below.";
|
"A contact with that DID is already in your contact list. Edit them directly below.";
|
||||||
}
|
}
|
||||||
|
if (err.name === "ConstraintError") {
|
||||||
|
message +=
|
||||||
|
" Check that the contact doesn't conflict with any you already have.";
|
||||||
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -653,7 +794,7 @@ export default class ContactsView extends Vue {
|
|||||||
async register(contact: Contact) {
|
async register(contact: Contact) {
|
||||||
if (
|
if (
|
||||||
confirm(
|
confirm(
|
||||||
"Are you sure you want to use one of your registrations for " +
|
"Are you sure you want to register " +
|
||||||
this.nameForDid(this.contacts, contact.did) +
|
this.nameForDid(this.contacts, contact.did) +
|
||||||
(contact.registered
|
(contact.registered
|
||||||
? " -- especially since they are already marked as registered"
|
? " -- especially since they are already marked as registered"
|
||||||
@@ -729,9 +870,11 @@ export default class ContactsView extends Vue {
|
|||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "success",
|
||||||
title: "Registration Success",
|
title: "Registration Success",
|
||||||
text: contact.name + " has been registered.",
|
text:
|
||||||
|
(contact.name || "That unnamed person") +
|
||||||
|
" has been registered.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -774,7 +917,7 @@ export default class ContactsView extends Vue {
|
|||||||
(visibility
|
(visibility
|
||||||
? "Are you sure you want to make your activity visible to them?"
|
? "Are you sure you want to make your activity visible to them?"
|
||||||
: "Are you sure you want to hide all your activity from them?");
|
: "Are you sure you want to hide all your activity from them?");
|
||||||
if (visibilityPrompt && confirm(visibilityPrompt)) {
|
if (!visibilityPrompt || confirm(visibilityPrompt)) {
|
||||||
const url =
|
const url =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/report/" +
|
"/api/report/" +
|
||||||
@@ -903,7 +1046,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private nameForContact(contact?: Contact, capitalize?: boolean): string {
|
private nameForContact(contact?: Contact, capitalize?: boolean): string {
|
||||||
return contact?.name || (capitalize ? "T" : "t") + "this unnamed user";
|
return contact?.name || (capitalize ? "T" : "t") + "his unnamed user";
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
|
async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
|
||||||
@@ -948,7 +1091,7 @@ export default class ContactsView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Input Error",
|
title: "Input Error",
|
||||||
text: "Giving no hours or descrption does nothing.",
|
text: "Giving no hours or description does nothing.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -958,7 +1101,7 @@ export default class ContactsView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Status Error",
|
title: "Status Error",
|
||||||
text: "No identity is available.",
|
text: "No identifier is available.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -988,7 +1131,7 @@ export default class ContactsView extends Vue {
|
|||||||
"?",
|
"?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.createAndSubmitGive(
|
this.createAndSubmitContactGive(
|
||||||
identity,
|
identity,
|
||||||
fromDid,
|
fromDid,
|
||||||
toDid,
|
toDid,
|
||||||
@@ -1000,7 +1143,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// similar function is in endorserServer.ts
|
// similar function is in endorserServer.ts
|
||||||
private async createAndSubmitGive(
|
private async createAndSubmitContactGive(
|
||||||
identity: IIdentifier,
|
identity: IIdentifier,
|
||||||
fromDid: string,
|
fromDid: string,
|
||||||
toDid: string,
|
toDid: string,
|
||||||
@@ -1069,7 +1212,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error in createAndSubmitGive: ", error);
|
console.log("Error in createAndSubmitContactGive: ", 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) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Discover"></QuickNav>
|
<QuickNav selected="Discover" />
|
||||||
<TopMessage />
|
<TopMessage />
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
@@ -28,6 +28,20 @@
|
|||||||
<!-- Result Tabs -->
|
<!-- Result Tabs -->
|
||||||
<div class="text-center text-slate-500 border-b border-slate-300">
|
<div class="text-center text-slate-500 border-b border-slate-300">
|
||||||
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
@click="
|
||||||
|
projects = [];
|
||||||
|
isRemoteActive = false;
|
||||||
|
isLocalActive = false;
|
||||||
|
isStarActive = true;
|
||||||
|
"
|
||||||
|
v-bind:class="computedStarTabClassNames()"
|
||||||
|
>
|
||||||
|
<fa icon="star" class="fa-fw" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -35,6 +49,7 @@
|
|||||||
projects = [];
|
projects = [];
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
isRemoteActive = false;
|
isRemoteActive = false;
|
||||||
|
isStarActive = false;
|
||||||
searchLocal();
|
searchLocal();
|
||||||
"
|
"
|
||||||
v-bind:class="computedLocalTabClassNames()"
|
v-bind:class="computedLocalTabClassNames()"
|
||||||
@@ -51,13 +66,14 @@
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
v-bind:class="computedRemoteTabClassNames()"
|
|
||||||
@click="
|
@click="
|
||||||
projects = [];
|
projects = [];
|
||||||
isRemoteActive = true;
|
isRemoteActive = true;
|
||||||
isLocalActive = false;
|
isLocalActive = false;
|
||||||
|
isStarActive = false;
|
||||||
searchAll();
|
searchAll();
|
||||||
"
|
"
|
||||||
|
v-bind:class="computedRemoteTabClassNames()"
|
||||||
>
|
>
|
||||||
Anywhere
|
Anywhere
|
||||||
<span
|
<span
|
||||||
@@ -71,8 +87,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isStarActive && projects.length == 0">
|
||||||
|
<div class="mt-4 text-center">You have not starred any projects.</div>
|
||||||
|
</div>
|
||||||
<div v-if="isLocalActive">
|
<div v-if="isLocalActive">
|
||||||
<div>
|
<div class="mt-2 text-center">
|
||||||
<button
|
<button
|
||||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
@click="$router.push({ name: 'search-area' })"
|
@click="$router.push({ name: 'search-area' })"
|
||||||
@@ -103,11 +122,11 @@
|
|||||||
class="block py-4 flex gap-4"
|
class="block py-4 flex gap-4"
|
||||||
>
|
>
|
||||||
<div class="w-12">
|
<div class="w-12">
|
||||||
<EntityIcon
|
<ProjectIcon
|
||||||
:entityId="project.handleId"
|
:entityId="project.handleId"
|
||||||
:iconSize="48"
|
:iconSize="48"
|
||||||
class="block border border-slate-300 rounded-md"
|
class="block border border-slate-300 rounded-md"
|
||||||
></EntityIcon>
|
></ProjectIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
@@ -131,12 +150,18 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
|
|
||||||
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 { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import {
|
||||||
|
BoundingBox,
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
MASTER_SETTINGS_KEY,
|
||||||
|
Settings,
|
||||||
|
} from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { didInfo, ProjectData } from "@/libs/endorserServer";
|
import { didInfo, PlanData, PlanServerRecord } 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 ProjectIcon from "@/components/ProjectIcon.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
@@ -148,9 +173,10 @@ interface Notification {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QuickNav,
|
|
||||||
InfiniteScroll,
|
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
|
InfiniteScroll,
|
||||||
|
ProjectIcon,
|
||||||
|
QuickNav,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -162,23 +188,27 @@ export default class DiscoverView extends Vue {
|
|||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
searchTerms = "";
|
searchTerms = "";
|
||||||
projects: ProjectData[] = [];
|
projects: PlanData[] = [];
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
isRemoteActive = false;
|
isRemoteActive = false;
|
||||||
|
isStarActive = false;
|
||||||
localCount = -1;
|
localCount = -1;
|
||||||
remoteCount = -1;
|
remoteCount = -1;
|
||||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||||
|
starredProjects: Array<string> = [];
|
||||||
|
|
||||||
// make this function available to the Vue template
|
// make this function available to the Vue template
|
||||||
didInfo = didInfo;
|
didInfo = didInfo;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings: Settings =
|
||||||
this.activeDid = settings?.activeDid || "";
|
(await db.settings.get(MASTER_SETTINGS_KEY)) || DEFAULT_SETTINGS;
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.activeDid = (settings?.activeDid as string) || "";
|
||||||
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
|
this.starredProjects = settings?.starredProjects || [];
|
||||||
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
@@ -186,7 +216,9 @@ export default class DiscoverView extends Vue {
|
|||||||
const allAccounts = await accountsDB.accounts.toArray();
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
this.allMyDids = allAccounts.map((acc) => acc.did);
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
if (this.searchBox) {
|
if (this.starredProjects.length > 0) {
|
||||||
|
await this.searchStars();
|
||||||
|
} else if (this.searchBox) {
|
||||||
await this.searchLocal();
|
await this.searchLocal();
|
||||||
} else {
|
} else {
|
||||||
this.isLocalActive = false;
|
this.isLocalActive = false;
|
||||||
@@ -201,7 +233,9 @@ export default class DiscoverView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async searchSelected() {
|
public async searchSelected() {
|
||||||
if (this.isLocalActive) {
|
if (this.isStarActive) {
|
||||||
|
await this.searchStars();
|
||||||
|
} else if (this.isLocalActive) {
|
||||||
await this.searchLocal();
|
await this.searchLocal();
|
||||||
} else {
|
} else {
|
||||||
await this.searchAll();
|
await this.searchAll();
|
||||||
@@ -221,7 +255,7 @@ export default class DiscoverView extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +308,7 @@ export default class DiscoverView extends Vue {
|
|||||||
|
|
||||||
const results = await response.json();
|
const results = await response.json();
|
||||||
|
|
||||||
const plans: ProjectData[] = results.data;
|
const plans: PlanData[] = 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, issuerDid, rowid } = plan;
|
||||||
@@ -358,7 +392,7 @@ export default class DiscoverView extends Vue {
|
|||||||
|
|
||||||
if (results.data) {
|
if (results.data) {
|
||||||
if (beforeId) {
|
if (beforeId) {
|
||||||
const plans: ProjectData[] = results.data;
|
const plans: PlanData[] = results.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({
|
this.projects.push({
|
||||||
@@ -393,6 +427,71 @@ export default class DiscoverView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async searchStars() {
|
||||||
|
this.resetCounts();
|
||||||
|
|
||||||
|
if (this.starredProjects.length == 0) {
|
||||||
|
this.projects = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
const response = await fetch(
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/plans?handleIds=" +
|
||||||
|
encodeURIComponent(JSON.stringify(this.starredProjects)),
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await this.buildHeaders(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
const details = await response.text();
|
||||||
|
console.log("Problem with full search:", details);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: `There was a problem accessing the server. Try again later.`,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw details;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
|
||||||
|
const plans: PlanServerRecord[] = results.data;
|
||||||
|
if (plans) {
|
||||||
|
for (const plan of plans) {
|
||||||
|
const { name, description, handleId, issuerDid } = plan;
|
||||||
|
this.projects.push({ name, description, handleId, issuerDid });
|
||||||
|
}
|
||||||
|
this.remoteCount = this.projects.length;
|
||||||
|
} else {
|
||||||
|
throw JSON.stringify(results);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log("Error with feed load:", e);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: e.userMessage || "There was a problem retrieving projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data loader used by infinite scroller
|
* Data loader used by infinite scroller
|
||||||
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
@@ -420,19 +519,39 @@ export default class DiscoverView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public computedStarTabClassNames() {
|
||||||
|
return {
|
||||||
|
"inline-block": true,
|
||||||
|
"py-3": true,
|
||||||
|
"rounded-t-lg": true,
|
||||||
|
"border-b-2": true,
|
||||||
|
|
||||||
|
active: this.isStarActive,
|
||||||
|
"text-black": this.isStarActive,
|
||||||
|
"border-black": this.isStarActive,
|
||||||
|
"font-semibold": this.isStarActive,
|
||||||
|
|
||||||
|
"text-blue-600": !this.isStarActive,
|
||||||
|
"border-transparent": !this.isStarActive,
|
||||||
|
"hover:border-slate-400": !this.isStarActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public computedLocalTabClassNames() {
|
public computedLocalTabClassNames() {
|
||||||
return {
|
return {
|
||||||
"inline-block": true,
|
"inline-block": true,
|
||||||
"py-3": true,
|
"py-3": true,
|
||||||
"rounded-t-lg": true,
|
"rounded-t-lg": true,
|
||||||
"border-b-2": true,
|
"border-b-2": true,
|
||||||
|
|
||||||
active: this.isLocalActive,
|
active: this.isLocalActive,
|
||||||
"text-blue-600": this.isLocalActive,
|
"text-black": this.isLocalActive,
|
||||||
"border-blue-600": this.isLocalActive,
|
"border-black": this.isLocalActive,
|
||||||
"font-semibold": this.isLocalActive,
|
"font-semibold": this.isLocalActive,
|
||||||
|
|
||||||
|
"text-blue-600": !this.isLocalActive,
|
||||||
"border-transparent": !this.isLocalActive,
|
"border-transparent": !this.isLocalActive,
|
||||||
"hover:text-slate-600": !this.isLocalActive,
|
"hover:border-slate-400": !this.isLocalActive,
|
||||||
"hover:border-slate-300": !this.isLocalActive,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,13 +561,15 @@ export default class DiscoverView extends Vue {
|
|||||||
"py-3": true,
|
"py-3": true,
|
||||||
"rounded-t-lg": true,
|
"rounded-t-lg": true,
|
||||||
"border-b-2": true,
|
"border-b-2": true,
|
||||||
|
|
||||||
active: this.isRemoteActive,
|
active: this.isRemoteActive,
|
||||||
"text-blue-600": this.isRemoteActive,
|
"text-black": this.isRemoteActive,
|
||||||
"border-blue-600": this.isRemoteActive,
|
"border-black": this.isRemoteActive,
|
||||||
"font-semibold": this.isRemoteActive,
|
"font-semibold": this.isRemoteActive,
|
||||||
|
|
||||||
|
"text-blue-600": !this.isRemoteActive,
|
||||||
"border-transparent": !this.isRemoteActive,
|
"border-transparent": !this.isRemoteActive,
|
||||||
"hover:text-slate-600": !this.isRemoteActive,
|
"hover:border-slate-400": !this.isRemoteActive,
|
||||||
"hover:border-slate-300": !this.isRemoteActive,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,21 @@
|
|||||||
register others; later, you can create projects, too.
|
register others; later, you can create projects, too.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Note that there are limits to how many others each person can register,
|
Note that there are rate limits to how many others you can register,
|
||||||
so you may have to wait.
|
so it may take some time to register everyone you want. Take your time...
|
||||||
|
make it an opportunity to get to know their projects, and show your own.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
I had an identifier, but I reinstalled and I got a new one automatically.
|
||||||
|
How do I restore my old one?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Go
|
||||||
|
<router-link class="text-blue-500" to="/import-account">import your identifier</router-link>.
|
||||||
|
If you don't want the old one, click "Advanced" and check the box to erase it.
|
||||||
|
(The erase option only shows if you have exactly one identifier.
|
||||||
|
For more in-depth surgery, you'll have to erase data from the browser or reinstall.)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I add someone else?</h2>
|
<h2 class="text-xl font-semibold">How do I add someone else?</h2>
|
||||||
@@ -125,7 +138,7 @@
|
|||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I restore my data?</h2>
|
<h2 class="text-xl font-semibold">How do I restore my data?</h2>
|
||||||
<p>
|
<p>
|
||||||
There are two parts to restore your data: the identity secrets and the
|
There are two steps to restore your data: the identity secrets, then the
|
||||||
other data such as settings, contacts, etc.
|
other data such as settings, contacts, etc.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -148,7 +161,7 @@
|
|||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
|
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
|
||||||
click Advanced, and follow the instructions for Data Import.
|
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,8 +223,8 @@
|
|||||||
<p>
|
<p>
|
||||||
If you don't see anything associated with a person, this is typically
|
If you don't see anything associated with a person, this is typically
|
||||||
because they have not given you permission to see their information. Ask
|
because they have not given you permission to see their information. Ask
|
||||||
them to add you to their contact list and make sure the eye next to your
|
them to add you to their contact list, and ask specifically to make sure
|
||||||
name is open like this
|
the eye next to your name is open like this
|
||||||
<fa icon="eye" class="fa-fw" /> and not closed like this
|
<fa icon="eye" class="fa-fw" /> and not closed like this
|
||||||
<fa icon="eye-slash" class="fa-fw" />.
|
<fa icon="eye-slash" class="fa-fw" />.
|
||||||
</p>
|
</p>
|
||||||
@@ -231,6 +244,13 @@
|
|||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
How do I get higher limits?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Let's talk. Contact us (below).
|
||||||
|
</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>
|
||||||
@@ -301,9 +321,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Package from "../../package.json";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
|
import * as Package from "../../package.json";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import { ONBOARD_MESSAGE } from "@/libs/util";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -325,8 +347,7 @@ export default class Help extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Onboard Someone",
|
title: "Onboard Someone",
|
||||||
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, preferably).
|
text: ONBOARD_MESSAGE,
|
||||||
text: "1) Check that they've entered their name. 2) Go to the scanning page via the Identity page and then the through the QR icon at the top, and then scan and register them. 3) Have them go to that page and scan you.",
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
<p style="display: inline; align-items: center">
|
<p style="display: inline; align-items: center">
|
||||||
This app doesn't support notifications, so let's fix that. <br />
|
This currently doesn't support notifications, so let's fix that.
|
||||||
|
<br />
|
||||||
<!-- Note that that exact verbiage shows in the help. -->
|
<!-- Note that that exact verbiage shows in the help. -->
|
||||||
|
|
||||||
<span v-if="userAgentInfo.getOS().name === 'iOS'">
|
<span v-if="userAgentInfo.getOS().name === 'iOS'">
|
||||||
@@ -44,14 +45,15 @@
|
|||||||
width="30"
|
width="30"
|
||||||
style="display: inline; margin: 0 5px; vertical-align: middle"
|
style="display: inline; margin: 0 5px; vertical-align: middle"
|
||||||
/>
|
/>
|
||||||
and go use that app.
|
and go use that app. If you already did these steps, reload this app
|
||||||
|
so that it is fully detected.
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
Try
|
Try
|
||||||
<a href="https://www.google.com/chrome/" class="text-blue-500"
|
<a href="https://www.google.com/chrome/" class="text-blue-500"
|
||||||
>Google Chrome</a
|
>Google Chrome</a
|
||||||
>
|
>
|
||||||
or look for a way to install as an app.
|
or look for a way to install as an app from this browser.
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,18 +61,24 @@
|
|||||||
|
|
||||||
<!-- show the actions for recognizing a give -->
|
<!-- show the actions for recognizing a give -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
|
<div v-if="isCreatingIdentifier">
|
||||||
|
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||||
|
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!activeDid"
|
v-if="!activeDid && !isCreatingIdentifier"
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
<p class="text-lg mb-3">
|
<p class="text-lg mb-3">
|
||||||
You need an <b>identifier</b> before you can record anyone's gives.
|
Want to connect with your contacts, or share contributions or
|
||||||
|
projects?
|
||||||
</p>
|
</p>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'start' }"
|
: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"
|
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
Create Your Identifier</router-link
|
Create An Identifier</router-link
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -78,33 +86,41 @@
|
|||||||
v-else-if="!isRegistered"
|
v-else-if="!isRegistered"
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
Someone must register your account before you can record anyone's gives.
|
Someone must register your identifier before you can record anyone's
|
||||||
To do this:
|
giving.
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contact-qr' }"
|
: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"
|
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
|
Show Them Your Identifier Info</router-link
|
||||||
>
|
>
|
||||||
<router-link
|
<br />
|
||||||
:to="{ name: 'account' }"
|
To double-check that you're registered,
|
||||||
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
<br />
|
||||||
>
|
<router-link :to="{ name: 'account' }" class="text-blue-500">
|
||||||
2. Check Your Limits</router-link
|
see your Usage Limits on the Account
|
||||||
|
<fa icon="circle-user" /> page.</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>
|
<div class="flex justify-between mb-4">
|
||||||
|
<h2 class="text-xl font-bold">Record Something Given</h2>
|
||||||
|
<button
|
||||||
|
@click="openGiftedPrompts()"
|
||||||
|
class="block text-center text-md font-bold bg-blue-500 text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Ideas...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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()">
|
||||||
<EntityIcon
|
<img
|
||||||
:entityId="null"
|
src="../assets/blank-square.svg"
|
||||||
:iconSize="64"
|
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
></EntityIcon>
|
/>
|
||||||
<h3
|
<h3
|
||||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
>
|
>
|
||||||
@@ -112,7 +128,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-for="contact in allContacts"
|
v-for="contact in allContacts.slice(0, 7)"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
@click="openDialog(contact)"
|
@click="openDialog(contact)"
|
||||||
>
|
>
|
||||||
@@ -120,7 +136,7 @@
|
|||||||
:entityId="contact.did"
|
:entityId="contact.did"
|
||||||
:iconSize="64"
|
:iconSize="64"
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
></EntityIcon>
|
/>
|
||||||
<h3
|
<h3
|
||||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
>
|
>
|
||||||
@@ -153,6 +169,7 @@
|
|||||||
message="Received from"
|
message="Received from"
|
||||||
showGivenToUser="true"
|
showGivenToUser="true"
|
||||||
/>
|
/>
|
||||||
|
<GiftedPrompts ref="giftedPrompts" />
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- 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">
|
||||||
@@ -168,20 +185,40 @@
|
|||||||
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
|
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"
|
v-if="record.jwtId == feedLastViewedClaimId"
|
||||||
>
|
>
|
||||||
You've seen all the following
|
You've seen all the following before
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="grid grid-cols-12">
|
||||||
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
|
<span class="col-span-11 justify-self-start">
|
||||||
<span class="">{{ this.giveDescription(record) }}</span>
|
<fa
|
||||||
<a @click="onClickLoadClaim(record.jwtId)">
|
icon="gift"
|
||||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
class="col-span-1 pt-1 pr-2 text-slate-500"
|
||||||
</a>
|
></fa>
|
||||||
|
{{ this.giveDescription(record) }}
|
||||||
|
<a @click="onClickLoadClaim(record.jwtId)">
|
||||||
|
<fa
|
||||||
|
icon="circle-info"
|
||||||
|
class="pl-2 text-blue-500 cursor-pointer"
|
||||||
|
></fa>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="col-span-1 justify-self-end shrink">
|
||||||
|
<router-link
|
||||||
|
v-if="record.fulfillsPlanHandleId"
|
||||||
|
:to="
|
||||||
|
'/project/' +
|
||||||
|
encodeURIComponent(record.fulfillsPlanHandleId)
|
||||||
|
"
|
||||||
|
class="justify-end"
|
||||||
|
>
|
||||||
|
<fa icon="hammer" class="ml-4 pl-2 text-blue-500"></fa>
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
<div :class="{ hidden: isHiddenSpinner }">
|
<div v-if="isFeedLoading">
|
||||||
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||||
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
||||||
</p>
|
</p>
|
||||||
@@ -196,6 +233,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
|
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import GiftedPrompts from "@/components/GiftedPrompts.vue";
|
||||||
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 TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
@@ -210,6 +248,7 @@ import {
|
|||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
import { generateSaveAndActivateIdentity } from "@/libs/util";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -221,6 +260,7 @@ interface Notification {
|
|||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
GiftedDialog,
|
GiftedDialog,
|
||||||
|
GiftedPrompts,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
@@ -234,19 +274,14 @@ export default class HomeView extends Vue {
|
|||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
feedData = [];
|
feedData: GiveServerRecord[] = [];
|
||||||
feedPreviousOldestId?: string;
|
feedPreviousOldestId?: string;
|
||||||
feedLastViewedClaimId?: string;
|
feedLastViewedClaimId?: string;
|
||||||
isHiddenSpinner = true;
|
isCreatingIdentifier = false;
|
||||||
|
isFeedLoading = true;
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
numAccounts = 0;
|
|
||||||
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
|
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
|
||||||
|
|
||||||
async beforeCreate() {
|
|
||||||
await accountsDB.open();
|
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const account = (await accountsDB.accounts
|
const account = (await accountsDB.accounts
|
||||||
@@ -280,12 +315,20 @@ export default class HomeView extends Vue {
|
|||||||
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
|
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
|
|
||||||
|
if (this.allMyDids.length === 0) {
|
||||||
|
this.isCreatingIdentifier = true;
|
||||||
|
this.activeDid = await generateSaveAndActivateIdentity();
|
||||||
|
this.allMyDids = [this.activeDid];
|
||||||
|
this.isCreatingIdentifier = false;
|
||||||
|
}
|
||||||
|
|
||||||
// this returns a Promise but we don't need to wait for it
|
// this returns a Promise but we don't need to wait for it
|
||||||
this.updateAllFeed();
|
|
||||||
|
await 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);
|
console.log("Error retrieving settings or feed.", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -293,7 +336,7 @@ 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 your settings or the latest activity.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -319,7 +362,7 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +384,7 @@ export default class HomeView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updateAllFeed() {
|
public async updateAllFeed() {
|
||||||
this.isHiddenSpinner = false;
|
this.isFeedLoading = true;
|
||||||
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
||||||
.then(async (results) => {
|
.then(async (results) => {
|
||||||
if (results.data.length > 0) {
|
if (results.data.length > 0) {
|
||||||
@@ -372,7 +415,7 @@ export default class HomeView extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.isHiddenSpinner = true;
|
this.isFeedLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -384,7 +427,9 @@ export default class HomeView extends Vue {
|
|||||||
public async retrieveGives(endorserApiServer: string, beforeId?: string) {
|
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?giftNotTrade=true&" +
|
||||||
|
beforeQuery,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: await this.buildHeaders(),
|
headers: await this.buildHeaders(),
|
||||||
@@ -464,5 +509,9 @@ export default class HomeView extends Vue {
|
|||||||
openDialog(giver: GiverInputInfo) {
|
openDialog(giver: GiverInputInfo) {
|
||||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openGiftedPrompts() {
|
||||||
|
(this.$refs.giftedPrompts as GiftedPrompts).open();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,13 +18,23 @@
|
|||||||
<!-- Identity List -->
|
<!-- Identity List -->
|
||||||
|
|
||||||
<!-- Current Identity - Display First! -->
|
<!-- Current Identity - Display First! -->
|
||||||
<div class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4">
|
<div
|
||||||
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
v-if="activeDid && !activeDidInIdentities"
|
||||||
<span class="overflow-hidden">
|
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4"
|
||||||
<div class="text-sm text-slate-500 truncate">
|
>
|
||||||
|
<fa icon="circle-check" class="fa-fw text-red-600 text-xl mr-3"></fa>
|
||||||
|
<div class="text-sm text-slate-500">
|
||||||
|
<div class="overflow-hidden truncate">
|
||||||
<b>ID:</b> <code>{{ activeDid }}</code>
|
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
<b
|
||||||
|
>There is a data corruption error: this identity is selected but it is
|
||||||
|
not in storage. You cannot send any more claims with this identity
|
||||||
|
until you import the seed again. This may require reinstalling the
|
||||||
|
app; if you know how, you can also clear out the TimeSafariAccounts
|
||||||
|
IndexedDB. Be sure to back up all your Settings & Contacts first.</b
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Other Identity/ies -->
|
<!-- Other Identity/ies -->
|
||||||
@@ -35,7 +45,12 @@
|
|||||||
:key="ident.did"
|
:key="ident.did"
|
||||||
@click="switchAccount(ident.did)"
|
@click="switchAccount(ident.did)"
|
||||||
>
|
>
|
||||||
<fa icon="circle" class="fa-fw text-slate-400 text-xl mr-3"></fa>
|
<fa
|
||||||
|
v-if="ident.did === activeDid"
|
||||||
|
icon="circle-check"
|
||||||
|
class="fa-fw text-blue-600 text-xl mr-3"
|
||||||
|
/>
|
||||||
|
<fa v-else icon="circle" class="fa-fw text-slate-400 text-xl mr-3" />
|
||||||
<span class="overflow-hidden">
|
<span class="overflow-hidden">
|
||||||
<h2 class="text-xl font-semibold mb-0"></h2>
|
<h2 class="text-xl font-semibold mb-0"></h2>
|
||||||
<div class="text-sm text-slate-500 truncate">
|
<div class="text-sm text-slate-500 truncate">
|
||||||
@@ -85,21 +100,12 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
Constants = AppString;
|
Constants = AppString;
|
||||||
public accounts: typeof AccountsSchema;
|
public accounts: typeof AccountsSchema;
|
||||||
public activeDid = "";
|
public activeDid = "";
|
||||||
|
public activeDidInIdentities = false;
|
||||||
public apiServer = "";
|
public apiServer = "";
|
||||||
public apiServerInput = "";
|
public apiServerInput = "";
|
||||||
public otherIdentities: Array<{ did: string }> = [];
|
public otherIdentities: Array<{ did: string }> = [];
|
||||||
public showContactGives = false;
|
public showContactGives = false;
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
|
||||||
await accountsDB.open();
|
|
||||||
const account = await accountsDB.accounts
|
|
||||||
.where("did")
|
|
||||||
.equals(activeDid)
|
|
||||||
.first();
|
|
||||||
const identity = JSON.parse((account?.identity as string) || "null");
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -109,19 +115,13 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
this.apiServerInput = settings?.apiServer || "";
|
this.apiServerInput = settings?.apiServer || "";
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
|
||||||
|
|
||||||
if (identity) {
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
activeDid: identity.did,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
for (let n = 0; n < accounts.length; n++) {
|
for (let n = 0; n < accounts.length; n++) {
|
||||||
const did = JSON.parse(accounts[n].identity)["did"];
|
const did = JSON.parse(accounts[n].identity)["did"];
|
||||||
if (did && this.activeDid !== did) {
|
this.otherIdentities.push({ did: did });
|
||||||
this.otherIdentities.push({ did: did });
|
if (did && this.activeDid === did) {
|
||||||
|
this.activeDidInIdentities = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -147,16 +147,6 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
activeDid: did,
|
activeDid: did,
|
||||||
});
|
});
|
||||||
this.activeDid = did || "";
|
|
||||||
this.otherIdentities = [];
|
|
||||||
await accountsDB.open();
|
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
for (let n = 0; n < accounts.length; n++) {
|
|
||||||
const did = JSON.parse(accounts[n].identity)["did"];
|
|
||||||
if (did && this.activeDid !== did) {
|
|
||||||
this.otherIdentities.push({ did: did });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
>
|
>
|
||||||
<fa icon="chevron-left"></fa>
|
<fa icon="chevron-left"></fa>
|
||||||
</button>
|
</button>
|
||||||
Import Existing Identity
|
Import Existing Identifier
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<!-- Import Account Form -->
|
<!-- Import Account Form -->
|
||||||
<p class="text-center text-xl mb-4 font-light">
|
<p class="text-center text-xl mb-4 font-light">
|
||||||
Enter your seed phrase below to import your identity on this device.
|
Enter your seed phrase below to import your identifier on this device.
|
||||||
</p>
|
</p>
|
||||||
<!-- id used by puppeteer test script -->
|
<!-- id used by puppeteer test script -->
|
||||||
<input
|
<input
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
v-model="mnemonic"
|
v-model="mnemonic"
|
||||||
/>
|
/>
|
||||||
{{ mnemonic }}
|
|
||||||
<h3
|
<h3
|
||||||
class="text-sm uppercase font-semibold mb-3"
|
class="text-sm uppercase font-semibold mb-3"
|
||||||
@click="showAdvanced = !showAdvanced"
|
@click="showAdvanced = !showAdvanced"
|
||||||
@@ -36,17 +36,28 @@
|
|||||||
Enter a custom derivation path
|
Enter a custom derivation path
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
v-model="derivationPath"
|
v-model="derivationPath"
|
||||||
/>
|
/>
|
||||||
For previous uPort or Endorser users,
|
<span class="ml-4">
|
||||||
<a @click="derivationPath = UPORT_DERIVATION_PATH" class="text-blue-500">
|
For previous uPort or Endorser users,
|
||||||
click here to use that value.
|
<a
|
||||||
</a>
|
@click="derivationPath = UPORT_DERIVATION_PATH"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
click here to use that value.
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="mt-4" v-if="numAccounts == 1">
|
||||||
|
<input type="checkbox" class="mr-2" v-model="shouldErase" />
|
||||||
|
<label>Erase the previous identifier.</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<button
|
<button
|
||||||
@click="from_mnemonic()"
|
@click="fromMnemonic()"
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
@@ -72,26 +83,42 @@ import {
|
|||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ImportAccountView extends Vue {
|
export default class ImportAccountView extends Vue {
|
||||||
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
|
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
|
||||||
|
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
mnemonic = "";
|
mnemonic = "";
|
||||||
address = "";
|
address = "";
|
||||||
|
numAccounts = 0;
|
||||||
privateHex = "";
|
privateHex = "";
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
|
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
|
||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
|
shouldErase = false;
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async from_mnemonic() {
|
public async fromMnemonic() {
|
||||||
const mne: string = this.mnemonic.trim().toLowerCase();
|
const mne: string = this.mnemonic.trim().toLowerCase();
|
||||||
if (this.mnemonic.trim().length > 0) {
|
try {
|
||||||
[this.address, this.privateHex, this.publicHex] = deriveAddress(
|
[this.address, this.privateHex, this.publicHex] = deriveAddress(
|
||||||
mne,
|
mne,
|
||||||
this.derivationPath,
|
this.derivationPath,
|
||||||
@@ -104,25 +131,48 @@ export default class ImportAccountView extends Vue {
|
|||||||
this.derivationPath,
|
this.derivationPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
await accountsDB.open();
|
||||||
await accountsDB.open();
|
if (this.shouldErase) {
|
||||||
await accountsDB.accounts.add({
|
await accountsDB.accounts.clear();
|
||||||
dateCreated: new Date().toISOString(),
|
}
|
||||||
derivationPath: this.derivationPath,
|
await accountsDB.accounts.add({
|
||||||
did: newId.did,
|
dateCreated: new Date().toISOString(),
|
||||||
identity: JSON.stringify(newId),
|
derivationPath: this.derivationPath,
|
||||||
mnemonic: mne,
|
did: newId.did,
|
||||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
identity: JSON.stringify(newId),
|
||||||
});
|
mnemonic: mne,
|
||||||
|
publicKeyHex: newId.keys[0].publicKeyHex,
|
||||||
|
});
|
||||||
|
|
||||||
// record that as the active DID
|
// record that as the active DID
|
||||||
await db.open();
|
await db.open();
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
activeDid: newId.did,
|
activeDid: newId.did,
|
||||||
});
|
});
|
||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
} catch (err) {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
console.error("Error saving mnemonic & updating settings:", err);
|
} catch (err: any) {
|
||||||
|
console.error("Error saving mnemonic & updating settings:", err);
|
||||||
|
if (err == "Error: invalid mnemonic") {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Mnemonic",
|
||||||
|
text: "Please check your mnemonic and try again.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Got an error creating that identifier.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
||||||
<!-- Breadcrumb -->
|
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
|
||||||
<!-- Cancel -->
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'project' }"
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
><fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
Make Commitment
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Project Details -->
|
|
||||||
|
|
||||||
<select class="block w-full rounded border border-slate-400 mb-4 px-3 py-2">
|
|
||||||
<option disabled>Choose a commitment type…</option>
|
|
||||||
<option selected>Time</option>
|
|
||||||
<option>Cryptocurrency</option>
|
|
||||||
<option>Money</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<!-- Time amount -->
|
|
||||||
<div class="mb-4 flex items-stretch">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="0.0"
|
|
||||||
class="block w-full rounded-l border border-slate-400 px-3 py-2"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="px-4 py-2 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
|
||||||
>hours</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Crypto amount -->
|
|
||||||
|
|
||||||
<!-- Money amount -->
|
|
||||||
|
|
||||||
<div class="mt-8">
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
|
||||||
value="Commit"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
|
||||||
>
|
|
||||||
Maybe Later
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {},
|
|
||||||
})
|
|
||||||
export default class NewEditCommitmentView extends Vue {}
|
|
||||||
</script>
|
|
||||||
@@ -29,6 +29,23 @@
|
|||||||
v-model="fullClaim.name"
|
v-model="fullClaim.name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Other Authorized Representative"
|
||||||
|
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||||
|
v-model="agentDid"
|
||||||
|
/>
|
||||||
|
<div class="mb-4">
|
||||||
|
<p v-if="activeDid != projectIssuerDid && agentDid != projectIssuerDid">
|
||||||
|
<span class="text-red-500">Beware!</span>
|
||||||
|
If you save this, the original project owner will no longer be able to
|
||||||
|
edit it.
|
||||||
|
<button @click="agentDid = projectIssuerDid" class="text-blue-500">
|
||||||
|
Click here to make the original owner an authorized representative.
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
@@ -43,6 +60,7 @@
|
|||||||
<input
|
<input
|
||||||
v-model="fullClaim.url"
|
v-model="fullClaim.url"
|
||||||
placeholder="Website"
|
placeholder="Website"
|
||||||
|
autocapitalize="none"
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -57,8 +75,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
||||||
<div class="px-2 py-2">
|
<div class="px-2 py-2">
|
||||||
For your security, we recommend you choose a location nearby but not
|
For your security, choose a location nearby but not exactly at the
|
||||||
exactly at the place.
|
place.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<l-map
|
<l-map
|
||||||
@@ -98,7 +116,7 @@
|
|||||||
<span :class="{ hidden: isHiddenSpinner }">
|
<span :class="{ hidden: isHiddenSpinner }">
|
||||||
<!-- icon no worky? -->
|
<!-- icon no worky? -->
|
||||||
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
|
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
|
||||||
Saving…</span
|
Saving...</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -141,6 +159,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
agentDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
fullClaim: PlanVerifiableCredential = {
|
fullClaim: PlanVerifiableCredential = {
|
||||||
@@ -150,9 +169,14 @@ export default class NewEditProjectView extends Vue {
|
|||||||
description: "",
|
description: "",
|
||||||
}; // this default is only to avoid errors before plan is loaded
|
}; // this default is only to avoid errors before plan is loaded
|
||||||
includeLocation = false;
|
includeLocation = false;
|
||||||
|
isHiddenSave = false;
|
||||||
|
isHiddenSpinner = true;
|
||||||
|
lastClaimJwtId = "";
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
projectId = localStorage.getItem("projectId") || "";
|
||||||
|
projectIssuerDid = "";
|
||||||
zoom = 2;
|
zoom = 2;
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
@@ -166,11 +190,11 @@ export default class NewEditProjectView extends Vue {
|
|||||||
.where("did")
|
.where("did")
|
||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first();
|
.first();
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse((account?.identity as string) || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to load project records with no identity available.",
|
"Attempted to load project records with no identifier available.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
@@ -185,15 +209,11 @@ export default class NewEditProjectView extends Vue {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
projectId = localStorage.getItem("projectId") || "";
|
|
||||||
isHiddenSave = false;
|
|
||||||
isHiddenSpinner = true;
|
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
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 as string) || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
|
|
||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
if (this.numAccounts === 0) {
|
if (this.numAccounts === 0) {
|
||||||
@@ -202,15 +222,15 @@ export default class NewEditProjectView extends Vue {
|
|||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.LoadProject(identity);
|
this.loadProject(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async LoadProject(identity: IIdentifier) {
|
async loadProject(identity: IIdentifier) {
|
||||||
const url =
|
const url =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/claim/byHandle/" +
|
"/api/claim/byHandle/" +
|
||||||
@@ -224,23 +244,33 @@ export default class NewEditProjectView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
|
this.projectIssuerDid = resp.data.issuer;
|
||||||
this.fullClaim = resp.data.claim;
|
this.fullClaim = resp.data.claim;
|
||||||
|
this.lastClaimJwtId = resp.data.id;
|
||||||
if (this.fullClaim?.location) {
|
if (this.fullClaim?.location) {
|
||||||
this.includeLocation = true;
|
this.includeLocation = true;
|
||||||
this.latitude = this.fullClaim.location.geo.latitude;
|
this.latitude = this.fullClaim.location.geo.latitude;
|
||||||
this.longitude = this.fullClaim.location.geo.longitude;
|
this.longitude = this.fullClaim.location.geo.longitude;
|
||||||
}
|
}
|
||||||
|
if (this.fullClaim?.agent?.identifier) {
|
||||||
|
this.agentDid = this.fullClaim.agent.identifier;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Got error retrieving that project", error);
|
console.error("Got error retrieving that project", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async SaveProject(identity: IIdentifier) {
|
private async saveProject(identity: IIdentifier) {
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim: PlanVerifiableCredential = this.fullClaim;
|
const vcClaim: PlanVerifiableCredential = this.fullClaim;
|
||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
vcClaim.identifier = this.projectId;
|
vcClaim.lastClaimId = this.lastClaimJwtId;
|
||||||
|
}
|
||||||
|
if (this.agentDid) {
|
||||||
|
vcClaim.agent = {
|
||||||
|
identifier: this.agentDid,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (this.includeLocation) {
|
if (this.includeLocation) {
|
||||||
vcClaim.location = {
|
vcClaim.location = {
|
||||||
@@ -283,25 +313,16 @@ export default class NewEditProjectView extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
if (resp.data?.success?.handleId) {
|
||||||
// version shows up here: https://api.endorser.ch/api-docs/
|
|
||||||
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
|
||||||
this.errorMessage = "";
|
this.errorMessage = "";
|
||||||
|
|
||||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
useAppStore()
|
||||||
// version shows up here: https://api.endorser.ch/api-docs/
|
.setProjectId(resp.data.success.handleId)
|
||||||
useAppStore().setProjectId(
|
.then(() => {
|
||||||
resp.data.success.handleId || resp.data.success.fullIri,
|
this.$router.push({ name: "project" });
|
||||||
);
|
});
|
||||||
setTimeout(
|
|
||||||
function (that: NewEditProjectView) {
|
|
||||||
that.$router.push({ name: "project" });
|
|
||||||
},
|
|
||||||
2000,
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.error(
|
||||||
"Got unexpected 'data' inside response from server",
|
"Got unexpected 'data' inside response from server",
|
||||||
resp,
|
resp,
|
||||||
);
|
);
|
||||||
@@ -321,9 +342,11 @@ export default class NewEditProjectView extends Vue {
|
|||||||
error?: { message?: string };
|
error?: { message?: string };
|
||||||
}>;
|
}>;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
console.log("Got error from server", serverError);
|
console.error("Got error from server", serverError);
|
||||||
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
||||||
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
|
userMessage =
|
||||||
|
(serverError.response?.data?.error?.message as string) ||
|
||||||
|
userMessage;
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -373,7 +396,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
console.error("Error: there is no account.");
|
console.error("Error: there is no account.");
|
||||||
} else {
|
} else {
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
this.SaveProject(identity);
|
this.saveProject(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { generateSaveAndActivateIdentity } from "@/libs/util";
|
||||||
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
|
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
@@ -64,28 +62,7 @@ export default class NewIdentifierView extends Vue {
|
|||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const mnemonic = generateSeed();
|
await generateSaveAndActivateIdentity();
|
||||||
// address is 0x... ETH address, without "did:eth:"
|
|
||||||
const [address, privateHex, publicHex, derivationPath] =
|
|
||||||
deriveAddress(mnemonic);
|
|
||||||
|
|
||||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
|
||||||
const identity = JSON.stringify(newId);
|
|
||||||
|
|
||||||
await accountsDB.open();
|
|
||||||
await accountsDB.accounts.add({
|
|
||||||
dateCreated: new Date().toISOString(),
|
|
||||||
derivationPath: derivationPath,
|
|
||||||
did: newId.did,
|
|
||||||
identity: identity,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
activeDid: newId.did,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$router.push({ name: "home" });
|
this.$router.push({ name: "home" });
|
||||||
|
|||||||
@@ -23,11 +23,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="block pb-4 flex gap-4">
|
<div class="block pb-4 flex gap-4">
|
||||||
<div class="flex-none w-16 pt-1">
|
<div class="flex-none w-16 pt-1">
|
||||||
<EntityIcon
|
<ProjectIcon
|
||||||
:entityId="projectId"
|
:entityId="projectId"
|
||||||
:iconSize="64"
|
:iconSize="64"
|
||||||
class="block border border-slate-300 rounded-md"
|
class="block border border-slate-300 rounded-md"
|
||||||
></EntityIcon>
|
></ProjectIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
@@ -35,7 +35,23 @@
|
|||||||
<div class="text-sm mb-3">
|
<div class="text-sm mb-3">
|
||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
{{ issuer }}
|
{{
|
||||||
|
serverUtil.didInfo(issuer, activeDid, allMyDids, allContacts)
|
||||||
|
}}
|
||||||
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
libsUtil.doCopyTwoSecRedo(
|
||||||
|
issuer,
|
||||||
|
() => (showDidCopy = !showDidCopy),
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="ml-2 mr-2"
|
||||||
|
>
|
||||||
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
<span v-show="showDidCopy">Copied DID</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="timeSince">
|
<div v-if="timeSince">
|
||||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||||
@@ -80,9 +96,14 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a @click="onClickLoadClaim(projectId)" class="cursor-pointer">
|
||||||
|
<fa icon="circle-info" class="pl-2 pt-1 text-blue-500" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="issuer == activeDid"
|
v-if="activeDid === issuer || activeDid === agentDid"
|
||||||
type="button"
|
type="button"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
@click="onEditClick()"
|
@click="onEditClick()"
|
||||||
@@ -94,32 +115,39 @@
|
|||||||
<div v-if="activeDid" class="mb-4">
|
<div v-if="activeDid" class="mb-4">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<button
|
||||||
@click="openOfferDialog({ name: 'you', did: activeDid })"
|
@click="openOfferDialog()"
|
||||||
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 offer…
|
Offer (maybe with conditions)...
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<OfferDialog ref="customOfferDialog" :projectId="this.projectId" />
|
||||||
|
|
||||||
<div v-if="activeDid">
|
<div v-if="activeDid">
|
||||||
|
<GiftedDialog
|
||||||
|
ref="customGiveDialog"
|
||||||
|
message="Received from"
|
||||||
|
:projectId="this.projectId"
|
||||||
|
>
|
||||||
|
</GiftedDialog>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button
|
<p class="mt-2 mb-4 text-center">Record a contribution from:</p>
|
||||||
@click="openGiftDialog({ name: 'you', did: activeDid })"
|
|
||||||
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
|
||||||
>
|
|
||||||
I gave…
|
|
||||||
</button>
|
|
||||||
<p class="mt-2 mb-4 text-center">Or, record a contribution from:</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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({ name: 'you', did: activeDid })">
|
||||||
|
<fa icon="hand" class="fa-fw text-slate-400 text-5xl" />
|
||||||
|
<h3
|
||||||
|
class="mt-5 text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
You
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
<li @click="openGiftDialog()">
|
<li @click="openGiftDialog()">
|
||||||
<EntityIcon
|
<img
|
||||||
:entityId="null"
|
src="../assets/blank-square.svg"
|
||||||
:iconSize="64"
|
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
></EntityIcon>
|
/>
|
||||||
<h3
|
<h3
|
||||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
>
|
>
|
||||||
@@ -127,7 +155,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-for="contact in allContacts"
|
v-for="contact in allContacts.slice(0, 6)"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
@click="openGiftDialog(contact)"
|
@click="openGiftDialog(contact)"
|
||||||
>
|
>
|
||||||
@@ -135,7 +163,7 @@
|
|||||||
:entityId="contact.did"
|
:entityId="contact.did"
|
||||||
:iconSize="64"
|
:iconSize="64"
|
||||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
></EntityIcon>
|
/>
|
||||||
<h3
|
<h3
|
||||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
>
|
>
|
||||||
@@ -145,13 +173,13 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||||
<router-link
|
<a
|
||||||
v-if="allContacts.length >= 7"
|
v-if="allContacts.length >= 7"
|
||||||
:to="{ name: 'contact-gives' }"
|
@click="onClickAllContactsGifting()"
|
||||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
Show More Contacts…
|
Show More Contacts…
|
||||||
</router-link>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gifts to & from this -->
|
<!-- Gifts to & from this -->
|
||||||
@@ -162,7 +190,10 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div v-if="offersToThis.length === 0">
|
<div v-if="offersToThis.length === 0">
|
||||||
(None yet. Record one above.)
|
(None yet. Wanna
|
||||||
|
<span @click="openOfferDialog()" class="cursor-pointer text-blue-500"
|
||||||
|
>offer something... especially if others join you</span
|
||||||
|
>?)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
@@ -174,22 +205,43 @@
|
|||||||
<div class="flex justify-between gap-4">
|
<div class="flex justify-between gap-4">
|
||||||
<span>
|
<span>
|
||||||
<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) }}
|
{{
|
||||||
|
serverUtil.didInfo(
|
||||||
|
offer.offeredByDid,
|
||||||
|
activeDid,
|
||||||
|
allMyDids,
|
||||||
|
allContacts,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<a @click="onClickLoadClaim(offer.jwtId)">
|
<span v-if="offer.amount" class="whitespace-nowrap">
|
||||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
|
||||||
</a>
|
|
||||||
<span v-if="offer.amount">
|
|
||||||
<fa
|
<fa
|
||||||
:icon="iconForUnitCode(offer.unit)"
|
:icon="libsUtil.iconForUnitCode(offer.unit)"
|
||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
/>{{ offer.amount }}
|
/>{{ offer.amount }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="offer.objectDescription" class="text-slate-500">
|
<div v-if="offer.objectDescription" class="text-slate-500">
|
||||||
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
<fa icon="comment" class="fa-fw text-slate-400" />
|
||||||
{{ offer.objectDescription }}
|
{{ offer.objectDescription }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<a
|
||||||
|
@click="onClickLoadClaim(offer.jwtId as string)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
<fa icon="circle-info" class="pl-2 pt-1 text-blue-500" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="checkIsFulfillable(offer)"
|
||||||
|
@click="onClickFulfillGiveToOffer(offer)"
|
||||||
|
>
|
||||||
|
<fa
|
||||||
|
icon="hand-holding-heart"
|
||||||
|
class="text-blue-500 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +249,10 @@
|
|||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Given To This Idea</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Given To This Idea</h3>
|
||||||
|
|
||||||
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
|
<div v-if="givesToThis.length === 0">
|
||||||
|
(None yet. If you've seen something, say something by clicking a
|
||||||
|
contact above.)
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
@@ -208,22 +263,38 @@
|
|||||||
<div class="flex justify-between gap-4">
|
<div class="flex justify-between gap-4">
|
||||||
<span
|
<span
|
||||||
><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) }}
|
{{
|
||||||
|
serverUtil.didInfo(
|
||||||
|
give.agentDid,
|
||||||
|
activeDid,
|
||||||
|
allMyDids,
|
||||||
|
allContacts,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<a @click="onClickLoadClaim(give.jwtId)">
|
<span v-if="give.amount" class="whitespace-nowrap">
|
||||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
|
||||||
</a>
|
|
||||||
<span v-if="give.amount">
|
|
||||||
<fa
|
<fa
|
||||||
:icon="iconForUnitCode(give.unit)"
|
:icon="libsUtil.iconForUnitCode(give.unit)"
|
||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
/>{{ give.amount }}
|
/>{{ give.amount }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-slate-500">
|
||||||
|
<fa icon="calendar" class="fa-fw text-slate-400" />
|
||||||
|
{{ give.issuedAt?.substring(0, 10) }}
|
||||||
|
</div>
|
||||||
<div v-if="give.description" class="text-slate-500">
|
<div v-if="give.description" class="text-slate-500">
|
||||||
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
<fa icon="comment" class="fa-fw text-slate-400" />
|
||||||
{{ give.description }}
|
{{ give.description }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<a @click="onClickLoadClaim(give.jwtId)">
|
||||||
|
<fa icon="circle-info" class="text-blue-500 cursor-pointer" />
|
||||||
|
</a>
|
||||||
|
<a v-if="checkIsConfirmable(give)" @click="confirmClaim(give)">
|
||||||
|
<fa icon="circle-check" class="text-blue-500 cursor-pointer" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -251,7 +322,7 @@
|
|||||||
|
|
||||||
<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 From This Idea
|
||||||
</h3>
|
</h3>
|
||||||
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -265,15 +336,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiftedDialog
|
|
||||||
ref="customGiveDialog"
|
|
||||||
message="Received from"
|
|
||||||
:projectId="this.projectId"
|
|
||||||
>
|
|
||||||
</GiftedDialog>
|
|
||||||
<OfferDialog ref="customOfferDialog" :projectId="this.projectId">
|
|
||||||
</OfferDialog>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -290,16 +352,19 @@ 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";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { isGlobalUri } from "@/libs/util";
|
import * as libsUtil from "@/libs/util";
|
||||||
import {
|
import {
|
||||||
didInfo,
|
BLANK_GENERIC_SERVER_RECORD,
|
||||||
|
GenericServerRecord,
|
||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
OfferServerRecord,
|
OfferServerRecord,
|
||||||
PlanServerRecord,
|
PlanServerRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
|
import * as serverUtil from "@/libs/endorserServer";
|
||||||
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 ProjectIcon from "@/components/ProjectIcon.vue";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
@@ -310,12 +375,20 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
|
components: {
|
||||||
|
EntityIcon,
|
||||||
|
GiftedDialog,
|
||||||
|
OfferDialog,
|
||||||
|
ProjectIcon,
|
||||||
|
QuickNav,
|
||||||
|
TopMessage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class ProjectViewView extends Vue {
|
export default class ProjectViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
agentDid = "";
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
@@ -330,11 +403,15 @@ export default class ProjectViewView extends Vue {
|
|||||||
name = "";
|
name = "";
|
||||||
offersToThis: Array<OfferServerRecord> = [];
|
offersToThis: Array<OfferServerRecord> = [];
|
||||||
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
||||||
|
showDidCopy = false;
|
||||||
timeSince = "";
|
timeSince = "";
|
||||||
truncatedDesc = "";
|
truncatedDesc = "";
|
||||||
truncateLength = 40;
|
truncateLength = 40;
|
||||||
url = "";
|
url = "";
|
||||||
|
|
||||||
|
libsUtil = libsUtil;
|
||||||
|
serverUtil = serverUtil;
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
@@ -344,7 +421,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = accountsDB.accounts;
|
const accounts = accountsDB.accounts;
|
||||||
const accountsArr = await accounts?.toArray();
|
const accountsArr: Account[] = await accounts?.toArray();
|
||||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
@@ -353,7 +430,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
if (pathParam) {
|
if (pathParam) {
|
||||||
this.projectId = decodeURIComponent(pathParam);
|
this.projectId = decodeURIComponent(pathParam);
|
||||||
}
|
}
|
||||||
this.LoadProject(this.projectId, identity);
|
this.loadProject(this.projectId, identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
@@ -384,15 +461,6 @@ export default class ProjectViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Isn't there a better way to make this available to the template?
|
// 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 didInfo(did, activeDid, dids, contacts);
|
|
||||||
}
|
|
||||||
|
|
||||||
expandText() {
|
expandText() {
|
||||||
this.expanded = true;
|
this.expanded = true;
|
||||||
}
|
}
|
||||||
@@ -401,7 +469,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async LoadProject(projectId: string, identity: IIdentifier) {
|
async loadProject(projectId: string, identity: IIdentifier) {
|
||||||
this.projectId = projectId;
|
this.projectId = projectId;
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
@@ -423,6 +491,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
const now = moment.now();
|
const now = moment.now();
|
||||||
this.timeSince = moment.utc(now).to(eventDate);
|
this.timeSince = moment.utc(now).to(eventDate);
|
||||||
}
|
}
|
||||||
|
this.agentDid = resp.data.claim?.agent?.identifier;
|
||||||
this.issuer = resp.data.issuer;
|
this.issuer = resp.data.issuer;
|
||||||
this.name = resp.data.claim?.name || "(no name)";
|
this.name = resp.data.claim?.name || "(no name)";
|
||||||
this.description = resp.data.claim?.description || "(no description)";
|
this.description = resp.data.claim?.description || "(no description)";
|
||||||
@@ -432,7 +501,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.url = resp.data.claim?.url || "";
|
this.url = resp.data.claim?.url || "";
|
||||||
} else {
|
} else {
|
||||||
// actually, axios throws an error on 404 so we probably never get here
|
// actually, axios throws an error on 404 so we probably never get here
|
||||||
console.log("Error getting project:", resp);
|
console.error("Error getting project:", resp);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -471,7 +540,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
|
|
||||||
const givesInUrl =
|
const givesInUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/givesForPlans?planIds=" +
|
"/api/v2/report/givesToPlans?planIds=" +
|
||||||
encodeURIComponent(JSON.stringify([projectId]));
|
encodeURIComponent(JSON.stringify([projectId]));
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(givesInUrl, { headers });
|
const resp = await this.axios.get(givesInUrl, { headers });
|
||||||
@@ -624,7 +693,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
path: "/project/" + encodeURIComponent(projectId),
|
path: "/project/" + encodeURIComponent(projectId),
|
||||||
};
|
};
|
||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
this.LoadProject(projectId, await this.getIdentity(this.activeDid));
|
this.loadProject(projectId, await this.getIdentity(this.activeDid));
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenStreetMapUrl() {
|
getOpenStreetMapUrl() {
|
||||||
@@ -641,7 +710,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
openGiftDialog(contact: GiverInputInfo) {
|
openGiftDialog(contact?: GiverInputInfo) {
|
||||||
(this.$refs.customGiveDialog as GiftedDialog).open(contact);
|
(this.$refs.customGiveDialog as GiftedDialog).open(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,6 +718,14 @@ export default class ProjectViewView extends Vue {
|
|||||||
(this.$refs.customOfferDialog as OfferDialog).open();
|
(this.$refs.customOfferDialog as OfferDialog).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClickAllContactsGifting() {
|
||||||
|
localStorage.setItem("projectId", this.projectId);
|
||||||
|
const route = {
|
||||||
|
name: "contact-gives",
|
||||||
|
};
|
||||||
|
this.$router.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
onClickLoadClaim(jwtId: string) {
|
onClickLoadClaim(jwtId: string) {
|
||||||
const route = {
|
const route = {
|
||||||
path: "/claim/" + encodeURIComponent(jwtId),
|
path: "/claim/" + encodeURIComponent(jwtId),
|
||||||
@@ -656,28 +733,31 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
UNIT_CODES: Record<string, Record<string, string>> = {
|
checkIsFulfillable(offer: OfferServerRecord) {
|
||||||
BTC: {
|
const offerRecord: GenericServerRecord = {
|
||||||
name: "Bitcoin",
|
...BLANK_GENERIC_SERVER_RECORD,
|
||||||
faIcon: "bitcoin-sign",
|
claim: offer.fullClaim,
|
||||||
},
|
claimType: "Offer",
|
||||||
HUR: {
|
issuer: offer.offeredByDid,
|
||||||
name: "hours",
|
};
|
||||||
faIcon: "clock",
|
return libsUtil.canFulfillOffer(offerRecord);
|
||||||
},
|
}
|
||||||
USD: {
|
|
||||||
name: "US Dollars",
|
|
||||||
faIcon: "dollar",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
iconForUnitCode(unitCode: string) {
|
onClickFulfillGiveToOffer(offer: OfferServerRecord) {
|
||||||
return this.UNIT_CODES[unitCode]?.faIcon || "question";
|
const offerRecord: GenericServerRecord = {
|
||||||
|
...BLANK_GENERIC_SERVER_RECORD,
|
||||||
|
claim: offer.fullClaim,
|
||||||
|
issuer: offer.offeredByDid,
|
||||||
|
};
|
||||||
|
const giver: GiverInputInfo = {
|
||||||
|
did: libsUtil.offerGiverDid(offerRecord),
|
||||||
|
};
|
||||||
|
(this.$refs.customGiveDialog as GiftedDialog).open(giver, offer.handleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return an HTTPS URL if it's not a global URL
|
// return an HTTPS URL if it's not a global URL
|
||||||
addScheme(url: string) {
|
addScheme(url: string) {
|
||||||
if (!isGlobalUri(url)) {
|
if (!libsUtil.isGlobalUri(url)) {
|
||||||
return "https://" + url;
|
return "https://" + url;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
@@ -702,5 +782,70 @@ export default class ProjectViewView extends Vue {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkIsConfirmable(give: GiveServerRecord) {
|
||||||
|
const giveDetails: GenericServerRecord = {
|
||||||
|
...BLANK_GENERIC_SERVER_RECORD,
|
||||||
|
claim: give.fullClaim,
|
||||||
|
claimType: "GiveAction",
|
||||||
|
issuer: give.agentDid,
|
||||||
|
};
|
||||||
|
return libsUtil.isGiveRecordTheUserCanConfirm(giveDetails, this.activeDid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// similar code is found in ClaimView
|
||||||
|
async confirmClaim(give: GiveServerRecord) {
|
||||||
|
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(
|
||||||
|
give.fullClaim,
|
||||||
|
give.jwtId,
|
||||||
|
give.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.error("Got error submitting the confirmation:", result);
|
||||||
|
const message =
|
||||||
|
(result.error?.error as string) ||
|
||||||
|
"There was a problem submitting the confirmation. See logs for more info.";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,8 +8,44 @@
|
|||||||
Your Ideas
|
Your Ideas
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Quick Search -->
|
<!-- Result Tabs -->
|
||||||
|
<div class="text-center text-slate-500 border-b border-slate-300">
|
||||||
|
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
@click="
|
||||||
|
offers = [];
|
||||||
|
projects = [];
|
||||||
|
showOffers = true;
|
||||||
|
showProjects = false;
|
||||||
|
loadOffers();
|
||||||
|
"
|
||||||
|
v-bind:class="computedOfferTabClassNames()"
|
||||||
|
>
|
||||||
|
Offers
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
@click="
|
||||||
|
offers = [];
|
||||||
|
projects = [];
|
||||||
|
showOffers = false;
|
||||||
|
showProjects = true;
|
||||||
|
loadProjects();
|
||||||
|
"
|
||||||
|
v-bind:class="computedProjectTabClassNames()"
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Search -->
|
||||||
|
<!--
|
||||||
<div id="QuickSearch" class="mb-4 flex">
|
<div id="QuickSearch" class="mb-4 flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -22,9 +58,11 @@
|
|||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- New Project -->
|
<!-- New Project -->
|
||||||
<button
|
<button
|
||||||
|
v-if="showProjects"
|
||||||
class="fixed right-6 bottom-24 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full"
|
class="fixed right-6 bottom-24 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full"
|
||||||
@click="onClickNewProject()"
|
@click="onClickNewProject()"
|
||||||
>
|
>
|
||||||
@@ -39,8 +77,108 @@
|
|||||||
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Offer Results List -->
|
||||||
<InfiniteScroll @reached-bottom="loadMoreData">
|
<InfiniteScroll v-if="showOffers" @reached-bottom="loadMoreOfferData">
|
||||||
|
<ul class="border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
class="border-b border-slate-300"
|
||||||
|
v-for="offer in offers"
|
||||||
|
:key="offer.handleId"
|
||||||
|
>
|
||||||
|
<div class="block py-4 flex gap-4">
|
||||||
|
<div v-if="offer.fulfillsPlanHandleId" class="flex-none w-12">
|
||||||
|
<ProjectIcon
|
||||||
|
:entityId="offer.fulfillsPlanHandleId"
|
||||||
|
:iconSize="48"
|
||||||
|
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||||
|
></ProjectIcon>
|
||||||
|
</div>
|
||||||
|
<div v-if="offer.recipientDid" class="flex-none w-12">
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="offer.recipientDid"
|
||||||
|
:iconSize="48"
|
||||||
|
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||||
|
></EntityIcon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{{ offer.objectDescription }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-sm">
|
||||||
|
<span v-if="offer.amount">
|
||||||
|
<fa
|
||||||
|
:icon="libsUtil.iconForUnitCode(offer.unit)"
|
||||||
|
class="fa-fw text-slate-400"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span v-if="offer.amountGiven >= offer.amount">
|
||||||
|
<fa icon="check-circle" class="fa-fw text-green-500" />
|
||||||
|
All {{ offer.amount }} given
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<fa
|
||||||
|
icon="triangle-exclamation"
|
||||||
|
class="fa-fw text-yellow-500"
|
||||||
|
/>
|
||||||
|
{{ offer.amountGiven ? "" : "All" }}
|
||||||
|
{{ offer.amount - (offer.amountGiven || 0) }} remaining
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="offer.amountGiven > 0">
|
||||||
|
<span class="text-sm text-slate-400">
|
||||||
|
({{ offer.amountGiven }} given,
|
||||||
|
<span
|
||||||
|
v-if="offer.amountGivenConfirmed >= offer.amountGiven"
|
||||||
|
>
|
||||||
|
<!-- no need for green icon; unnecessary if there's already a green, confusing if there's a yellow -->
|
||||||
|
all
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<!-- only show icon if there's not already a warning -->
|
||||||
|
<fa
|
||||||
|
v-if="offer.amountGiven >= offer.amount"
|
||||||
|
icon="triangle-exclamation"
|
||||||
|
class="fa-fw text-yellow-300"
|
||||||
|
/>
|
||||||
|
{{ offer.amountGivenConfirmed || 0 }}
|
||||||
|
</span>
|
||||||
|
of that is confirmed)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<!-- Non-amount offer -->
|
||||||
|
<span v-if="offer.nonAmountGivenConfirmed">
|
||||||
|
<fa icon="check-circle" class="fa-fw text-green-500" />
|
||||||
|
{{ offer.nonAmountGivenConfirmed }}
|
||||||
|
{{ offer.nonAmountGivenConfirmed == 1 ? "give" : "gives" }}
|
||||||
|
are confirmed.
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<fa
|
||||||
|
icon="triangle-exclamation"
|
||||||
|
class="fa-fw text-yellow-500"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">Not confirmed by anyone</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a @click="onClickLoadClaim(offer.jwtId)">
|
||||||
|
<fa
|
||||||
|
icon="circle-info"
|
||||||
|
class="pl-2 text-blue-500 cursor-pointer"
|
||||||
|
></fa>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</InfiniteScroll>
|
||||||
|
<!-- Project Results List -->
|
||||||
|
<InfiniteScroll v-if="showProjects" @reached-bottom="loadMoreProjectData">
|
||||||
<ul class="border-t border-slate-300">
|
<ul class="border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300"
|
class="border-b border-slate-300"
|
||||||
@@ -52,11 +190,11 @@
|
|||||||
class="block py-4 flex gap-4"
|
class="block py-4 flex gap-4"
|
||||||
>
|
>
|
||||||
<div class="flex-none w-12">
|
<div class="flex-none w-12">
|
||||||
<EntityIcon
|
<ProjectIcon
|
||||||
:entityId="project.handleId"
|
:entityId="project.handleId"
|
||||||
:iconSize="48"
|
:iconSize="48"
|
||||||
class="inline-block align-middle border border-slate-300 rounded-md"
|
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||||
></EntityIcon>
|
></ProjectIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
@@ -74,15 +212,18 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import * as libsUtil from "@/libs/util";
|
||||||
import { IIdentifier } from "@veramo/core";
|
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 ProjectIcon from "@/components/ProjectIcon.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { ProjectData } from "@/libs/endorserServer";
|
import { OfferServerRecord, PlanData } from "@/libs/endorserServer";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -92,20 +233,61 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage },
|
components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
|
||||||
})
|
})
|
||||||
export default class ProjectsView extends Vue {
|
export default class ProjectsView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
projects: ProjectData[] = [];
|
projects: PlanData[] = [];
|
||||||
current: IIdentifier;
|
currentIid: IIdentifier;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
offers: OfferServerRecord[] = [];
|
||||||
|
showOffers = true;
|
||||||
|
showProjects = false;
|
||||||
|
|
||||||
async beforeCreate() {
|
libsUtil = libsUtil;
|
||||||
await accountsDB.open();
|
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
/**
|
||||||
|
* 'created' hook runs when the Vue instance is first created
|
||||||
|
**/
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
const activeDid: string = (settings?.activeDid as string) || "";
|
||||||
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
if (this.numAccounts === 0) {
|
||||||
|
console.error("No accounts found.");
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You need an identifier to load your projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.currentIid = await this.getIdentity(activeDid);
|
||||||
|
await this.loadOffers();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error initializing:", err);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong loading your projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,7 +295,7 @@ export default class ProjectsView extends Vue {
|
|||||||
* @param url the url used to fetch the data
|
* @param url the url used to fetch the data
|
||||||
* @param token Authorization token
|
* @param token Authorization token
|
||||||
**/
|
**/
|
||||||
async dataLoader(url: string, token: string) {
|
async projectDataLoader(url: string, token: string) {
|
||||||
const headers: { [key: string]: string } = {
|
const headers: { [key: string]: string } = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -123,13 +305,17 @@ export default class ProjectsView extends Vue {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200 || !resp.data.data) {
|
if (resp.status === 200 || !resp.data.data) {
|
||||||
const plans: ProjectData[] = resp.data.data;
|
const plans: PlanData[] = resp.data.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Bad server response & data:", resp.status, resp.data);
|
console.error(
|
||||||
|
"Bad server response & data for plans:",
|
||||||
|
resp.status,
|
||||||
|
resp.data,
|
||||||
|
);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -142,7 +328,7 @@ export default class ProjectsView extends Vue {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Got error loading projects:", error.message || error);
|
console.error("Got error loading plans:", error.message || error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -161,15 +347,44 @@ export default class ProjectsView extends Vue {
|
|||||||
* Data loader used by infinite scroller
|
* Data loader used by infinite scroller
|
||||||
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
**/
|
**/
|
||||||
async loadMoreData(payload: boolean) {
|
async loadMoreProjectData(payload: boolean) {
|
||||||
if (this.projects.length > 0 && payload) {
|
if (this.projects.length > 0 && payload) {
|
||||||
const latestProject = this.projects[this.projects.length - 1];
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
const url = `${this.apiServer}/api/v2/report/plansByIssuer?beforeId=${latestProject.rowid}`;
|
await this.loadProjects(
|
||||||
const token = await accessToken(this.current);
|
this.currentIid,
|
||||||
await this.dataLoader(url, token);
|
`beforeId=${latestProject.rowid}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load projects initially
|
||||||
|
* @param identifier of the user
|
||||||
|
* @param urlExtra additional url parameters in a string
|
||||||
|
**/
|
||||||
|
async loadProjects(identifier?: IIdentifier, urlExtra: string = "") {
|
||||||
|
const identity = identifier || this.currentIid;
|
||||||
|
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
|
||||||
|
const token: string = await accessToken(identity);
|
||||||
|
await this.projectDataLoader(url, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid: string): Promise<IIdentifier> {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse((account?.identity as string) || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load project records with no identifier available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle clicking on a project entry found in the list
|
* Handle clicking on a project entry found in the list
|
||||||
* @param id of the project
|
* @param id of the project
|
||||||
@@ -182,72 +397,6 @@ export default class ProjectsView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load projects initially
|
|
||||||
* @param identity of the user
|
|
||||||
**/
|
|
||||||
async LoadProjects(identity: IIdentifier) {
|
|
||||||
const url = `${this.apiServer}/api/v2/report/plansByIssuer`;
|
|
||||||
const token: string = await accessToken(identity);
|
|
||||||
await this.dataLoader(url, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
|
||||||
await accountsDB.open();
|
|
||||||
const account = await accountsDB.accounts
|
|
||||||
.where("did")
|
|
||||||
.equals(activeDid)
|
|
||||||
.first();
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error(
|
|
||||||
"Attempted to load project records with no identity available.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 'created' hook runs when the Vue instance is first created
|
|
||||||
**/
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
const activeDid = settings?.activeDid || "";
|
|
||||||
this.apiServer = settings?.apiServer || "";
|
|
||||||
|
|
||||||
if (this.numAccounts === 0) {
|
|
||||||
console.error("No accounts found.");
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "You need an identity to load your projects.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const identity = await this.getIdentity(activeDid);
|
|
||||||
this.current = identity;
|
|
||||||
this.LoadProjects(identity);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Error initializing:", err);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Something went wrong loading your projects.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handling clicking on the new project button
|
* Handling clicking on the new project button
|
||||||
**/
|
**/
|
||||||
@@ -258,5 +407,120 @@ export default class ProjectsView extends Vue {
|
|||||||
};
|
};
|
||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClickLoadClaim(jwtId: string) {
|
||||||
|
const route = {
|
||||||
|
path: "/claim/" + encodeURIComponent(jwtId),
|
||||||
|
};
|
||||||
|
this.$router.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core offer data loader
|
||||||
|
* @param url the url used to fetch the data
|
||||||
|
* @param token Authorization token
|
||||||
|
**/
|
||||||
|
async offerDataLoader(url: string, token: string) {
|
||||||
|
const headers: { [key: string]: string } = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
const resp = await this.axios.get(url, { headers });
|
||||||
|
if (resp.status === 200 || !resp.data.data) {
|
||||||
|
this.offers = this.offers.concat(resp.data.data);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Bad server response & data for offers:",
|
||||||
|
resp.status,
|
||||||
|
resp.data,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to get offers from the server. Try again later.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Got error loading offers:", error.message || error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Got an error loading offers.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data loader used by infinite scroller
|
||||||
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
|
**/
|
||||||
|
async loadMoreOfferData(payload: boolean) {
|
||||||
|
if (this.offers.length > 0 && payload) {
|
||||||
|
const latestOffer = this.offers[this.offers.length - 1];
|
||||||
|
await this.loadOffers(this.currentIid, `&beforeId=${latestOffer.jwtId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load offers initially
|
||||||
|
* @param identifier of the user
|
||||||
|
* @param urlExtra additional url parameters in a string
|
||||||
|
**/
|
||||||
|
async loadOffers(identifier?: IIdentifier, urlExtra: string = "") {
|
||||||
|
const identity = identifier || this.currentIid;
|
||||||
|
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${identity.did}${urlExtra}`;
|
||||||
|
const token: string = await accessToken(identity);
|
||||||
|
await this.offerDataLoader(url, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedOfferTabClassNames() {
|
||||||
|
return {
|
||||||
|
"inline-block": true,
|
||||||
|
"py-3": true,
|
||||||
|
"rounded-t-lg": true,
|
||||||
|
"border-b-2": true,
|
||||||
|
|
||||||
|
active: this.showOffers,
|
||||||
|
"text-black": this.showOffers,
|
||||||
|
"border-black": this.showOffers,
|
||||||
|
"font-semibold": this.showOffers,
|
||||||
|
|
||||||
|
"text-blue-600": !this.showOffers,
|
||||||
|
"border-transparent": !this.showOffers,
|
||||||
|
"hover:border-slate-400": !this.showOffers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedProjectTabClassNames() {
|
||||||
|
return {
|
||||||
|
"inline-block": true,
|
||||||
|
"py-3": true,
|
||||||
|
"rounded-t-lg": true,
|
||||||
|
"border-b-2": true,
|
||||||
|
|
||||||
|
active: this.showProjects,
|
||||||
|
"text-black": this.showProjects,
|
||||||
|
"border-black": this.showProjects,
|
||||||
|
"font-semibold": this.showProjects,
|
||||||
|
|
||||||
|
"text-blue-600": !this.showProjects,
|
||||||
|
"border-transparent": !this.showProjects,
|
||||||
|
"hover:border-slate-400": !this.showProjects,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Back -->
|
||||||
|
<div class="text-lg text-center font-light relative px-7">
|
||||||
|
<h1
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
@click="$router.back()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Seed Backup
|
Seed Backup
|
||||||
@@ -29,10 +39,10 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-if="numAccounts > 1">
|
<p v-if="numAccounts > 1">
|
||||||
<b class="text-orange-600">Note:</b> You have more than one identity
|
<b class="text-orange-600">Note:</b> You have more than one identifier
|
||||||
stored in this browser. If they are all based on the same seed as the
|
stored in this browser. If they are all based on the same seed as the
|
||||||
current identity, this one backup is sufficient; however, if you have
|
current identifier, this one backup is sufficient; however, if you have
|
||||||
different seeds for other identities, you will have to back them up
|
different seeds for other identifiers, you will have to back them up
|
||||||
separately.
|
separately.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -49,7 +59,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>You do not have an active identity.</div>
|
<div v-else>You do not have an active identifier.</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -91,7 +101,7 @@ export default class SeedBackupView extends Vue {
|
|||||||
this.numAccounts = accounts.length;
|
this.numAccounts = accounts.length;
|
||||||
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error("Got an error loading an identity:", err);
|
console.error("Got an error loading an identifier:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
class="p-6 pb-24 min-h-screen flex flex-col justify-center"
|
class="p-6 pb-24 min-h-screen flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div>
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
<div class="text-lg text-center font-light relative px-7">
|
||||||
<h1
|
<h1
|
||||||
@@ -23,27 +23,35 @@
|
|||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
<!-- id used by puppeteer test script -->
|
||||||
<div id="start-question" class="mt-8">
|
<div id="start-question" class="mt-8">
|
||||||
<p class="text-center text-xl mb-4 font-light">
|
<p class="text-center text-xl font-light">
|
||||||
Do you have an identity to import?
|
Do you want a new identifier of your own?
|
||||||
|
</p>
|
||||||
|
<p class="text-center font-light">
|
||||||
|
If you haven't used this before, click "Yes" to generate a new
|
||||||
|
identifier.
|
||||||
|
</p>
|
||||||
|
<p class="text-center mb-4 font-light">
|
||||||
|
Only click "No" if you have a seed of 12 or 24 words generated
|
||||||
|
elsewhere.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
@click="onClickYes()"
|
@click="onClickYes()"
|
||||||
class="block w-full text-center text-lg uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
class="block w-full text-center text-lg uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
No
|
Yes, generate one
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@click="onClickNo()"
|
@click="onClickNo()"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
||||||
>
|
>
|
||||||
Yes
|
No, I have a seed
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="numAccounts > 0"
|
v-if="numAccounts > 0"
|
||||||
@click="onClickDerive()"
|
@click="onClickDerive()"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
||||||
>
|
>
|
||||||
Derive New Address from Seed Imported Previously
|
Derive new address from existing seed
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
29
sw_combine.js
Normal file
29
sw_combine.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* We've seen cases where the functions inside safari-notifications.js are not found.
|
||||||
|
* This is our attempt to ensure that all the functions are available.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const swScriptsDir = path.resolve(__dirname, "sw_scripts");
|
||||||
|
const outputFile = path.resolve(__dirname, "sw_scripts-combined.js");
|
||||||
|
|
||||||
|
// Read all files in the sw_scripts directory
|
||||||
|
fs.readdir(swScriptsDir, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Error reading directory:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine files content into one script
|
||||||
|
const combinedContent = files
|
||||||
|
.filter((file) => path.extname(file) === ".js")
|
||||||
|
.map((file) => fs.readFileSync(path.join(swScriptsDir, file), "utf8"))
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
// Write the combined content to the output file
|
||||||
|
fs.writeFileSync(outputFile, combinedContent, "utf8");
|
||||||
|
|
||||||
|
console.log("Service worker files combined.");
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-env serviceworker */
|
/* eslint-env serviceworker */
|
||||||
/* global workbox */
|
/* global workbox */
|
||||||
|
/* eslint-disable */ /* ... because old-browser-compatible files in this directory are combined into a single script during `npm run build` */
|
||||||
importScripts(
|
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",
|
||||||
);
|
);
|
||||||
@@ -7,7 +8,9 @@ importScripts(
|
|||||||
function logConsoleAndDb(message, arg1, arg2) {
|
function logConsoleAndDb(message, arg1, arg2) {
|
||||||
// in chrome://serviceworker-internals note that the arg1 and arg2 here will show as "[object Object]" in that page but will show as expandable objects in the console
|
// in chrome://serviceworker-internals note that the arg1 and arg2 here will show as "[object Object]" in that page but will show as expandable objects in the console
|
||||||
console.log(`${new Date().toISOString()} ${message}`, arg1, arg2);
|
console.log(`${new Date().toISOString()} ${message}`, arg1, arg2);
|
||||||
if (self.appendDailyLog) {
|
// appendDailyLog is injected at build time by the vue.config.js configureWebpack apply plugin
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
if (appendDailyLog) {
|
||||||
let fullMessage = `${new Date().toISOString()} ${message}`;
|
let fullMessage = `${new Date().toISOString()} ${message}`;
|
||||||
if (arg1) {
|
if (arg1) {
|
||||||
fullMessage += `\n${JSON.stringify(arg1)}`;
|
fullMessage += `\n${JSON.stringify(arg1)}`;
|
||||||
@@ -15,25 +18,19 @@ function logConsoleAndDb(message, arg1, arg2) {
|
|||||||
if (arg2) {
|
if (arg2) {
|
||||||
fullMessage += `\n${JSON.stringify(arg2)}`;
|
fullMessage += `\n${JSON.stringify(arg2)}`;
|
||||||
}
|
}
|
||||||
self.appendDailyLog(fullMessage);
|
// appendDailyLog is injected from safari-notifications.js at build time by the vue.config.js configureWebpack apply plugin
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
appendDailyLog(fullMessage);
|
||||||
} else {
|
} else {
|
||||||
// sometimes we get the error: "Uncaught TypeError: self.appendDailyLog is not a function"
|
// sometimes we get the error: "Uncaught TypeError: appendDailyLog is not a function"
|
||||||
console.log(
|
console.log(
|
||||||
"Not logging to DB (often because self.appendDailyLog doesn't exist).",
|
"Not logging to DB (often because appendDailyLog doesn't exist).",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener("install", async (event) => {
|
self.addEventListener("install", async (/* event */) => {
|
||||||
console.log("Service worker got install event. Importing scripts...", event);
|
logConsoleAndDb("Service worker finished installation.");
|
||||||
await importScripts(
|
|
||||||
"safari-notifications.js",
|
|
||||||
"nacl.js",
|
|
||||||
"noble-curves.js",
|
|
||||||
"noble-hashes.js",
|
|
||||||
);
|
|
||||||
// this should now be available
|
|
||||||
logConsoleAndDb("Service worker imported all scripts.");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("activate", (event) => {
|
self.addEventListener("activate", (event) => {
|
||||||
@@ -84,7 +81,9 @@ self.addEventListener("push", function (event) {
|
|||||||
} else {
|
} else {
|
||||||
title = payload.title || "Update";
|
title = payload.title || "Update";
|
||||||
}
|
}
|
||||||
message = await self.getNotificationCount();
|
// getNotificationCount is injected from safari-notifications.js at build time by the vue.config.js configureWebpack apply plugin
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
message = await getNotificationCount();
|
||||||
}
|
}
|
||||||
if (message) {
|
if (message) {
|
||||||
const options = {
|
const options = {
|
||||||
|
|||||||
@@ -547,7 +547,11 @@ async function getNotificationCount() {
|
|||||||
newClaims++;
|
newClaims++;
|
||||||
}
|
}
|
||||||
if (newClaims > 0) {
|
if (newClaims > 0) {
|
||||||
result = `There are ${newClaims} new activities on Time Safari`;
|
if (newClaims === 1) {
|
||||||
|
result = "There is 1 new activity on Time Safari";
|
||||||
|
} else {
|
||||||
|
result = `There are ${newClaims} new activities on Time Safari`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const most_recent_notified = claims[0]["id"];
|
const most_recent_notified = claims[0]["id"];
|
||||||
await setMostRecentNotified(most_recent_notified);
|
await setMostRecentNotified(most_recent_notified);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { defineConfig } = require("@vue/cli-service");
|
const { defineConfig } = require("@vue/cli-service");
|
||||||
const { gitDescribeSync } = require("git-describe");
|
const { gitDescribeSync } = require("git-describe");
|
||||||
|
const { exec } = require("child_process");
|
||||||
|
|
||||||
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
|
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
|
||||||
|
|
||||||
@@ -10,6 +11,23 @@ module.exports = defineConfig({
|
|||||||
experiments: {
|
experiments: {
|
||||||
topLevelAwait: true,
|
topLevelAwait: true,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
// Still don't know why this runs three times.
|
||||||
|
apply: (compiler) => {
|
||||||
|
compiler.hooks.beforeCompile.tap("BeforeCompile", () => {
|
||||||
|
// Execute combine-sw.js script
|
||||||
|
exec("node sw_combine.js", (error, stdout, stderr) => {
|
||||||
|
if (error || stderr) {
|
||||||
|
console.error("Service worker files error:", error || stderr);
|
||||||
|
} else {
|
||||||
|
console.log("Finished combining service worker files.", stdout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
pwa: {
|
pwa: {
|
||||||
iconPaths: {
|
iconPaths: {
|
||||||
@@ -17,7 +35,8 @@ module.exports = defineConfig({
|
|||||||
},
|
},
|
||||||
workboxPluginMode: "InjectManifest",
|
workboxPluginMode: "InjectManifest",
|
||||||
workboxOptions: {
|
workboxOptions: {
|
||||||
swSrc: "./sw_scripts/additional-scripts.js",
|
// this script will be checked for linting (sw_scripts/* files generate about 1000 linting errors)
|
||||||
|
swSrc: "./sw_scripts-combined.js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user