Compare commits

...

120 Commits

Author SHA1 Message Date
a1d23ff1b0 start display of starred projects (but without setting start & not sure about endpoint) 2024-02-17 10:04:31 -07:00
37690cc855 increment versiona and add "-beta" 2024-02-14 20:56:37 -07:00
5f9edea116 bump version to 0.2.14 2024-02-14 20:50:15 -07:00
f517b09ed7 combine all service-worker scripts into a single file to try and ensure included scripts aren't lost 2024-02-14 20:46:34 -07:00
ca70b19831 fix claim-view page when the claim argument is not a global ID 2024-02-12 20:10:18 -07:00
f41e541fe2 send the last JWT instead of the identifier for plan edits 2024-02-11 16:05:15 -07:00
5c547783a7 remove unused page; tweak task list 2024-02-11 07:14:16 -07:00
8d2dd6357a update readme 2024-02-09 09:08:26 -07:00
189261e991 update messaging for contact registration 2024-02-07 18:57:58 -07:00
15464602f9 bump to version 0.2.14-beta 2024-02-07 18:42:33 -07:00
331c4f64d6 add check for valid "did:" DIDs 2024-02-07 18:23:13 -07:00
28ae317958 refactor tasks & add more estimates 2024-02-05 09:03:55 -07:00
643718619e remove unnecessary logic in account switcher; refactor task list 2024-02-04 20:11:04 -07:00
c3819ec919 don't autocapitalize website input; refactor tasks 2024-02-03 19:33:52 -07:00
719e3a467d make a number input targeted towards numbers 2024-02-03 19:21:07 -07:00
b251d7e4fd change project icon to a hammer 2024-02-03 19:20:54 -07:00
61c3a0e30b avoid error on browsers without a service worker 2024-02-03 19:19:58 -07:00
a76df55224 add display of my own offers 2024-02-03 18:56:09 -07:00
e140da081f fix name derivation on give dialog 2024-02-03 18:10:46 -07:00
1be899c48d ensure error message shows, and unset register flag if there's an API error 2024-02-02 17:40:06 -07:00
6aee93ca6c update tasks; enhance an error message & some typescripts 2024-02-02 12:25:04 -07:00
5412625d05 increment version and add -beta; tweak tasks & tests 2024-02-02 10:22:51 -07:00
8f579b40a9 bump to verson 0.2.12 2024-02-01 12:12:13 -07:00
e8a907c63a add more thankfulness prompts 2024-02-01 12:09:09 -07:00
f53a6f3045 tweak the prompt for contacts to be able to skip them 2024-02-01 11:52:31 -07:00
b38ebc45e1 add a prompt for things for which to express gratitude 2024-01-31 21:15:40 -07:00
c51d2629b3 bump version and add -beta 2024-01-28 15:20:15 -07:00
e642b99ff5 bump version to 0.2.11 2024-01-28 15:04:12 -07:00
26f1e88f5a doc: update tests & tasks 2024-01-28 15:01:47 -07:00
2e164dfeff tweak messages for missing identifier 2024-01-28 13:21:21 -07:00
d7530ff56b adjust more UI on the Advanced section, and make other small code & UI tweaks 2024-01-27 17:32:17 -07:00
2db52cb72e fix default server display in advanced section & refactor UI 2024-01-27 14:49:51 -07:00
c8eb3bfbc0 move save & cancel buttons further apart 2024-01-27 14:15:14 -07:00
71b210d541 add to manual tests & changelog 2024-01-27 13:05:33 -07:00
66289ec206 update tasks 2024-01-27 13:02:47 -07:00
639dc7b4e5 add instruction to error output 2024-01-27 12:40:40 -07:00
4fe072f19e move DB logic out of 'created' in components since it's not needed yet 2024-01-27 08:27:52 -07:00
f253f0af0f add ability to import from Endorser Mobile CSV 2024-01-26 20:36:29 -07:00
2d95a35905 add date to project give record list; don't wrap icon & amount 2024-01-26 16:10:14 -07:00
88f869d600 lower project "I Gave" button into list of contact, and tweak other wording 2024-01-24 20:46:05 -07:00
a0911bb0fd add copy-paste icon next to non-anonymous, non-hidden DIDs on details page 2024-01-21 15:50:39 -07:00
1053b78ab8 add sharing & copying instructions when asking contacts for help, and list all the visibleTo DIDs with an English description of their path 2024-01-21 15:16:39 -07:00
dcfa8d9451 add first stab at showing how the contact is visible in my network 2024-01-20 20:33:51 -07:00
dd38f76ee1 increment version and add -beta; add to tasks and tests 2024-01-18 21:11:19 -07:00
667e1e8890 bump version to 0.2.10 2024-01-18 20:05:44 -07:00
1731f2443b update offer dialog to allow other units 2024-01-17 20:50:35 -07:00
e1cffcda2d fix problem where extended screen of contacts didn't pass project 2024-01-17 20:18:01 -07:00
a5b1b97012 show the identicon in large size on the contacts screen 2024-01-17 19:47:33 -07:00
563b5793a9 add different identicons for people (and increment version & add -beta) 2024-01-17 19:27:05 -07:00
660436c8fa add copy-did-to-clipboard on contact list 2024-01-16 19:58:18 -07:00
31a7752168 add link to project from gives on front page 2024-01-16 19:48:47 -07:00
3ebe7bc156 put didInfo names in more places and add copy icons for DIDs & IDs 2024-01-16 18:58:08 -07:00
0eb16d5661 add links for give & offer when they fulfill other things 2024-01-16 17:52:32 -07:00
edb09da10f add detailed-info button for a project 2024-01-16 15:31:55 -07:00
be6ec6745a show a 'give' button directly on offers in the ProjectView 2024-01-16 15:23:40 -07:00
b79c5fcf91 move info button for offer & add cursor for hover 2024-01-15 19:50:00 -07:00
9dea4066c9 add ability to confirm give directly from a project 2024-01-15 19:40:38 -07:00
9b586566f0 increment version and add "-beta" 2024-01-15 12:37:39 -07:00
e5e702f8a5 bump version to 0.2.9 2024-01-15 12:14:15 -07:00
32c9076c39 fix visibility after adding contact, and some messaging 2024-01-15 12:06:33 -07:00
6ab4c40fd0 bump to version 0.2.8 2024-01-14 21:03:22 -07:00
d7ef07c2e2 automatically create an identity on the first page (and other UI tweaks) 2024-01-14 21:00:59 -07:00
9f595040d8 fix problem with anonymous contributor; refine tasks 2024-01-14 15:27:57 -07:00
40a8794649 remove checks on old fullIri field 2024-01-13 18:48:29 -07:00
fa72d38d18 allow an agent to edit a project 2024-01-13 18:45:51 -07:00
31aacb286f reword prompt for creating an identifier on the start screen 2024-01-13 18:44:59 -07:00
2511f18fa7 enhance (& fix for mobile) styling and verbiage 2024-01-13 15:14:16 -07:00
febfa8b098 bump version to 0.2.7 2024-01-12 20:54:29 -07:00
e0fcb1f67b fix various verbiage 2024-01-12 20:40:46 -07:00
9183092325 fix the name of the offerer 2024-01-12 20:39:30 -07:00
a87179d127 change wording from "identity" to "identifier" in many places 2024-01-12 16:37:02 -07:00
14e203dd74 bump to version 0.2.6 2024-01-12 15:58:08 -07:00
acaaf8776d add ability to give to fulfill an offer; adjust visibility of claim actions 2024-01-12 15:54:45 -07:00
cb1f38c182 bump to verson 0.2.5, and edit tasks 2024-01-09 19:32:18 -07:00
cfa7466b94 show users when there's an error on the import page 2024-01-09 19:18:49 -07:00
f998364c72 update package-lock for previous bump 2024-01-09 18:31:53 -07:00
7b4f084b4b bump to version 0.2.4, update tasks 2024-01-09 18:31:11 -07:00
115329e26c update the onboarding help blurb 2024-01-09 18:30:55 -07:00
61bef57563 update some dependencies with moderate severity 2024-01-09 17:56:27 -07:00
a5368d0f82 update library with vulnerability 2024-01-09 17:54:37 -07:00
48cb45d230 bump version to 0.2.3, add endpoint name update 2024-01-09 17:49:41 -07:00
8a7ce0fe65 add flag for logging a contribution as a trade 2024-01-08 21:28:04 -07:00
525d3fc15a bump to version 0.2.2 2024-01-05 13:54:57 -07:00
68f3b79983 add hints for registration on the contact page 2024-01-05 13:50:35 -07:00
5353fe770a tweak verbiage and usability 2024-01-05 13:08:20 -07:00
60fec5763d bump version to 0.2.1 2024-01-05 12:48:38 -07:00
aeb1d6a6a5 add next-public-key-hash to manual input 2024-01-05 12:44:28 -07:00
ec6175a550 make a confirmation for contact visibility 2024-01-05 12:14:56 -07:00
c1361e088f render full claim details in a more resonable format of YAML not JSON 2024-01-05 11:20:16 -07:00
a2c986951e doc: refactor project tasks 2024-01-05 11:11:06 -07:00
dce7b8e3d9 add terms & conditions, and a note about data in this service 2024-01-05 10:34:13 -07:00
211e0487fe add verbiage for other non-Chrome cases 2024-01-05 09:56:29 -07:00
cc931dcb04 add notification check with instructions on front screen 2024-01-05 09:48:15 -07:00
bfe14cc9c2 increment version and add -beta 2024-01-04 10:40:15 -07:00
275dba4468 bump version to 0.2.0 2024-01-04 10:35:32 -07:00
1f05e81b05 add notification immediately after setting up subscription, and tweak messaging 2024-01-03 21:27:13 -07:00
e9ad68f2a5 add maskable images 2024-01-03 18:32:38 -07:00
934664b9c9 add the hashed-next-key to the contact data, shown & stored 2024-01-03 17:44:41 -07:00
780be59c76 remove name from identity switcher (since they are not tied to a DID) 2024-01-02 19:53:19 -07:00
4a0bedb628 fix one more list-outside indent 2024-01-02 19:27:13 -07:00
5689f95230 change list-inside to list-outside 2024-01-02 19:21:57 -07:00
3083bb084a add more notification help instructions; remove confusing, big name-edit button 2024-01-02 19:16:54 -07:00
821d27a58a Merge pull request 'Set max screen content width' (#102) from app-screen-max-width into master
Reviewed-on: #102
2024-01-02 10:24:09 -05:00
Jose Olarte III
998a1d312f Set max screen content width 2024-01-02 15:25:51 +08:00
1f13bf772c move wait for service-worker initialization into the notification modal 2024-01-01 20:36:23 -07:00
def744b3df don't allow notification service-worker interaction until it is ready 2024-01-01 20:04:37 -07:00
0fb37acb24 increment version and add -beta 2024-01-01 19:38:07 -07:00
20bb723f0b bump to version 0.1.9 2024-01-01 19:34:34 -07:00
d821a7bd59 Merge pull request 'make a backup download for browsers that don't get it automatically' (#101) from another-download into master
Reviewed-on: #101
2024-01-01 21:28:22 -05:00
9f3b7314e8 Merge branch 'master' into another-download 2024-01-01 19:27:52 -07:00
4df7bb58a4 add help for clearing data, plus some other help fixes 2024-01-01 17:09:30 -07:00
15ccd2394f add missing 'date' to log interface 2024-01-01 16:25:23 -07:00
920c7bb612 adjust look & message on DB download 2024-01-01 16:24:36 -07:00
6eb26ea90c remove IndexedDB keys that shouldn't be keys, and remove unused table, and add commentary 2024-01-01 16:24:30 -07:00
28b6d9bbf9 doc: reorganize top tasks 2023-12-31 19:58:51 -05:00
7a099183ae remove db.close calls that caused an error when trying to download 2023-12-29 13:14:35 -07:00
11070755d6 refine error message when duplicate contact is input 2023-12-29 13:10:07 -07:00
c79dfac1fe add a way to import contacts & settings 2023-12-29 12:46:47 -07:00
2b06c64664 make a backup download for browsers that don't get it automatically 2023-12-28 14:42:56 -07:00
769a928b3d increment version & add "-beta" 2023-12-27 20:03:59 -07:00
61 changed files with 4126 additions and 1469 deletions

2
.gitignore vendored
View File

@@ -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

View File

@@ -5,11 +5,103 @@ 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.1.8] - 2023.12.27 ## [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
- Check for notification capability on front screen
- Contact next-public-key-hash in manual textual input
- Confirmation for contact visibility change
- YAML rendering of full claim details
- Hints for onboarding on the contact screen
## [0.2.0] - 2024.01.04
### Added
- Contact next-public-key-hash
- Icon for Android
- More thorough messaging and testing for notifications
## [0.1.9] - 2024.01.01
### Added
- Import for contacts and settings
- Second download button for DuckDuckGo
### Changed
- Removed some keys from Dexie's IndexedDB declarations
## [0.1.8] - 2023.12.27- d26d1d360152a7d0e559b68486e85b72b88bd9ff
### Added ### Added
- DB logging for service-worker events - DB logging for service-worker events
- Help page for notifications - Help page for notifications

6
CONTRIBUTING.md Normal file
View 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).

131
README.md
View File

@@ -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.
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit.
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test".
* `npm run build`
* `npx prettier --write ./sw_scripts/` * `npx prettier --write ./sw_scripts/`
...to make sure the service worker scripts are in proper form. It's only important if you changed something in that directory. * Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit.
* `cp sw_scripts/[ns]* dist/` * [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js. ... 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).
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari` * 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.
* Revert src/constants/app.ts and/or package.json, edit package.json to increment version & add "-beta", `npm install`, and commit. * `npm run build`
* Get on the server and back up the time-safari folder.
* `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. 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,50 +76,67 @@ 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
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.) * Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.)
* Clear notification permission. (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".) * Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".)
* Unregister service worker. (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.) * Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.)
* Clear Cache Storage. (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.) * Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
(If you find more, add them to the HelpNotificationsView.vue file.) (If you find more, add them to the HelpNotificationsView.vue file.)
## 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/)

451
package-lock.json generated
View File

@@ -1,19 +1,22 @@
{ {
"name": "TimeSafari_Test", "name": "TimeSafari_Test",
"version": "0.1.8", "version": "0.2.15-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TimeSafari_Test", "name": "TimeSafari_Test",
"version": "0.1.8", "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",
"@fortawesome/vue-fontawesome": "^3.0.3", "@fortawesome/vue-fontawesome": "^3.0.3",
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
"@types/js-yaml": "^4.0.9",
"@veramo/core": "^5.4.1", "@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.4.1", "@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.4.1", "@veramo/data-store": "^5.4.1",
@@ -36,9 +39,10 @@
"git-describe": "^4.1.1", "git-describe": "^4.1.1",
"jdenticon": "^3.2.0", "jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"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",
@@ -50,6 +54,7 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"three": "^0.156.1", "three": "^0.156.1",
"ua-parser-js": "^1.0.37",
"util": "^0.12.5", "util": "^0.12.5",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
@@ -62,6 +67,7 @@
"@types/leaflet": "^1.9.4", "@types/leaflet": "^1.9.4",
"@types/ramda": "^0.29.3", "@types/ramda": "^0.29.3",
"@types/three": "^0.155.1", "@types/three": "^0.155.1",
"@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0", "@typescript-eslint/parser": "^6.6.0",
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",
@@ -2419,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",
@@ -8793,11 +9151,15 @@
"@types/istanbul-lib-report": "*" "@types/istanbul-lib-report": "*"
} }
}, },
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="
},
"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",
@@ -8975,6 +9337,12 @@
"integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==", "integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==",
"dev": true "dev": true
}, },
"node_modules/@types/ua-parser-js": {
"version": "0.7.39",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
"integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
"dev": true
},
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.18", "version": "0.0.18",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz",
@@ -10438,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",
@@ -11061,8 +11429,7 @@
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"devOptional": true
}, },
"node_modules/array-buffer-byte-length": { "node_modules/array-buffer-byte-length": {
"version": "1.0.0", "version": "1.0.0",
@@ -11260,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"
} }
@@ -11811,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"
@@ -12643,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",
@@ -12940,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",
@@ -15868,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",
@@ -16300,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",
@@ -18610,7 +18974,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"devOptional": true,
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },
@@ -20057,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"
}, },
@@ -21131,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"
}, },
@@ -22173,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"
} }
@@ -24398,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"
}, },
@@ -26206,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",
@@ -26834,9 +27213,9 @@
} }
}, },
"node_modules/ua-parser-js": { "node_modules/ua-parser-js": {
"version": "1.0.36", "version": "1.0.37",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
"integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==", "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -26851,8 +27230,6 @@
"url": "https://github.com/sponsors/faisalman" "url": "https://github.com/sponsors/faisalman"
} }
], ],
"optional": true,
"peer": true,
"engines": { "engines": {
"node": "*" "node": "*"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "TimeSafari_Test", "name": "TimeSafari_Test",
"version": "0.1.8", "version": "0.2.15-beta",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@@ -8,12 +8,15 @@
"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",
"@fortawesome/vue-fontawesome": "^3.0.3", "@fortawesome/vue-fontawesome": "^3.0.3",
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
"@types/js-yaml": "^4.0.9",
"@veramo/core": "^5.4.1", "@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.4.1", "@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.4.1", "@veramo/data-store": "^5.4.1",
@@ -36,9 +39,10 @@
"git-describe": "^4.1.1", "git-describe": "^4.1.1",
"jdenticon": "^3.2.0", "jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"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",
@@ -50,6 +54,7 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"three": "^0.156.1", "three": "^0.156.1",
"ua-parser-js": "^1.0.37",
"util": "^0.12.5", "util": "^0.12.5",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
@@ -62,6 +67,7 @@
"@types/leaflet": "^1.9.4", "@types/leaflet": "^1.9.4",
"@types/ramda": "^0.29.3", "@types/ramda": "^0.29.3",
"@types/three": "^0.155.1", "@types/three": "^0.155.1",
"@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0", "@typescript-eslint/parser": "^6.6.0",
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",

View File

@@ -1,54 +1,89 @@
tasks: tasks :
- 08 notifications : - 01 release server & client for fix to project-editing
- .2 after turning on notification, don't wait in push server but wait in client for message test
- insert tooling (exportable logs?) so that we can see problems and troubleshoot as we onboard
- if navigator.serviceWorker is null, then tell the user to wait
- Local install works after cleared out cache in Chrome
- fix maskable icon - .5 fix timeSafari.org cert renewals
- .2 anchor hash into BTC
- .1 add step 1 to onboarding hints to "install"
- give users next public key hash - 01 bookmarks for BVC
- .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui - 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
- 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
- make server endpoint for full English description of limits
- create a help-desk document & add screenshots
- show VC details... somehow: - .1 update "offer" units to have same functionality as "give" units
- 01 show my VCs - most interesting, or via search - 01 on home page, prompt for install check in addition to "supports notifications" check (since they won't get notified if Chrome is closed)
- 01 allow download of each VC (& confirmations, to show that they actually own their data) - 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list)
- 04 allow user to download VCs, mine + ones I can see about me from others - bug (that is hard to reproduce) - got error adding on Firefox user #0 as contact for themselves
- add VC confirmation?
- Release Minimum Viable Product :
- .5 deploy endorser.ch server above Dec 1 (to get plan searches by names as well as descriptions)
- 08 thorough testing for errors & edge cases
- 01 ensure ability to recover server remotely, and add redundant access
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
- Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- 02 watch for the service worker activation before showing the button to turn on notifications
- 01 server - show all claim details when issued by the issuer
- bug - got error adding on Firefox user #0 as contact for themselves
- bug (that is hard to reproduce) - back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity
- bug (that is hard to reproduce) - in Chrome, install app then delete app and try from Chrome browser and see log errors "Uncaught TypeError: self.appendDailyLog is not a function" - bug (that is hard to reproduce) - 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
@@ -59,13 +94,15 @@ 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).
- .2 show error to user when adding a duplicate contact - .2 show error to user when adding a duplicate contact
- 01 parse input more robustly (with CSV lib and not commas) - 01 parse input more robustly (with CSV lib and not commas)
- stats v1 : - stats v1 :
- 01 show numeric stats - 01 show numeric stats
- 04 show different graphic for projects vs people (gnome?) on world - 04 show different graphic for projects vs people (gnome?) on world
@@ -83,13 +120,15 @@ tasks:
- 32 accept images for contacts - 32 accept images for contacts
- import project interactions from GitHub/GitLab and manage signing - import project interactions from GitHub/GitLab and manage signing
- show total time offered to & fulfilled to a project
- show total time offered by & fulfilled by a contact
- linking between projects or plans : - linking between projects or plans :
- show total time given to & from a project - show total time given to & from a project
- terminology: - terminology:
- 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
@@ -101,28 +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
- 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -156,11 +156,17 @@
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg" class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
> >
<div class="w-full px-6 py-6 text-slate-900 text-center"> <div class="w-full px-6 py-6 text-slate-900 text-center">
<p class="text-lg mb-4"> <p v-if="serviceWorkerReady" class="text-lg mb-4">
Would you like to <b>turn on</b> notifications for this app? Would you like to <b>turn on</b> notifications for this app?
</p> </p>
<p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 10
seconds...
<fa icon="spinner" spin />
</p>
<button <button
v-if="serviceWorkerReady"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
@click=" @click="
close(notification.id); close(notification.id);
@@ -169,6 +175,7 @@
> >
Turn on Notifications Turn on Notifications
</button> </button>
<button <button
@click="close(notification.id)" @click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@@ -284,6 +291,7 @@ interface VapidResponse {
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { sendTestThroughPushServer } from "@/libs/util";
interface Notification { interface Notification {
group: string; group: string;
@@ -297,6 +305,7 @@ export default class App extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
b64 = ""; b64 = "";
serviceWorkerReady = false;
async mounted() { async mounted() {
try { try {
@@ -343,6 +352,10 @@ export default class App extends Vue {
); );
} }
} }
// there may be a long pause here on first initialization
navigator.serviceWorker?.ready.then(() => {
this.serviceWorkerReady = true;
});
} }
private sendMessageToServiceWorker( private sendMessageToServiceWorker(
@@ -430,28 +443,39 @@ 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();
}) })
.then((subscription) => { .then(async (subscription) => {
if (subscription) { if (subscription) {
return this.sendSubscriptionToServer(subscription); await this.$notify(
{
group: "alert",
type: "info",
title: "Notification Setup Underway",
text: "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page.",
},
-1,
);
this.sendSubscriptionToServer(subscription);
return subscription;
} else { } else {
throw new Error("Subscription object is not available."); throw new Error("Subscription object is not available.");
} }
}) })
.then(() => { .then(async (subscription) => {
console.log( console.log(
"Subscription data sent to server and all finished successfully.", "Subscription data sent to server and all finished successfully.",
); );
await sendTestThroughPushServer(subscription, true);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Notifications Turned On", title: "Notifications Turned On",
text: "Notifications are on. You should see one on your device; if not, see the 'Troubleshoot' page.", text: "Notifications are on. You should see at least one on your device; if not, check the 'Troubleshoot' page.",
}, },
-1, -1,
); );
@@ -551,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();
}) })

View 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

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" enable-background="new 0 0 50 50">
<path d="M30.3 13.7L25 8.4l-5.3 5.3-1.4-1.4L25 5.6l6.7 6.7z"/><path d="M24 7h2v21h-2z"/><path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="37.785" cy="28.501" r="28.836"/>
<path d="M37.441-3.5c8.951,0,16.572,3.125,22.857,9.372c3.008,3.009,5.295,6.448,6.857,10.314
c1.561,3.867,2.344,7.971,2.344,12.314c0,4.381-0.773,8.486-2.314,12.313c-1.543,3.828-3.82,7.21-6.828,10.143
c-3.123,3.085-6.666,5.448-10.629,7.086c-3.961,1.638-8.057,2.457-12.285,2.457s-8.276-0.808-12.143-2.429
c-3.866-1.618-7.333-3.961-10.4-7.027c-3.067-3.066-5.4-6.524-7-10.372S5.5,32.767,5.5,28.5c0-4.229,0.809-8.295,2.428-12.2
c1.619-3.905,3.972-7.4,7.057-10.486C21.08-0.394,28.565-3.5,37.441-3.5z M37.557,2.272c-7.314,0-13.467,2.553-18.458,7.657
c-2.515,2.553-4.448,5.419-5.8,8.6c-1.354,3.181-2.029,6.505-2.029,9.972c0,3.429,0.675,6.734,2.029,9.913
c1.353,3.183,3.285,6.021,5.8,8.516c2.514,2.496,5.351,4.399,8.515,5.715c3.161,1.314,6.476,1.971,9.943,1.971
c3.428,0,6.75-0.665,9.973-1.999c3.219-1.335,6.121-3.257,8.713-5.771c4.99-4.876,7.484-10.99,7.484-18.344
c0-3.543-0.648-6.895-1.943-10.057c-1.293-3.162-3.18-5.98-5.654-8.458C50.984,4.844,44.795,2.272,37.557,2.272z M37.156,23.187
l-4.287,2.229c-0.458-0.951-1.019-1.619-1.685-2c-0.667-0.38-1.286-0.571-1.858-0.571c-2.856,0-4.286,1.885-4.286,5.657
c0,1.714,0.362,3.084,1.085,4.113c0.724,1.029,1.791,1.544,3.201,1.544c1.867,0,3.181-0.915,3.944-2.743l3.942,2
c-0.838,1.563-2,2.791-3.486,3.686c-1.484,0.896-3.123,1.343-4.914,1.343c-2.857,0-5.163-0.875-6.915-2.629
c-1.752-1.752-2.628-4.19-2.628-7.313c0-3.048,0.886-5.466,2.657-7.257c1.771-1.79,4.009-2.686,6.715-2.686
C32.604,18.558,35.441,20.101,37.156,23.187z M55.613,23.187l-4.229,2.229c-0.457-0.951-1.02-1.619-1.686-2
c-0.668-0.38-1.307-0.571-1.914-0.571c-2.857,0-4.287,1.885-4.287,5.657c0,1.714,0.363,3.084,1.086,4.113
c0.723,1.029,1.789,1.544,3.201,1.544c1.865,0,3.18-0.915,3.941-2.743l4,2c-0.875,1.563-2.057,2.791-3.541,3.686
c-1.486,0.896-3.105,1.343-4.857,1.343c-2.896,0-5.209-0.875-6.941-2.629c-1.736-1.752-2.602-4.19-2.602-7.313
c0-3.048,0.885-5.466,2.658-7.257c1.77-1.79,4.008-2.686,6.713-2.686C51.117,18.558,53.938,20.101,55.613,23.187z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="64px" height="64px" viewBox="-0.5 0.5 64 64" enable-background="new -0.5 0.5 64 64" xml:space="preserve">
<g>
<circle fill="#FFFFFF" cx="31.325" cy="32.873" r="30.096"/>
<path id="text2809_1_" d="M31.5,14.08c-10.565,0-13.222,9.969-13.222,18.42c0,8.452,2.656,18.42,13.222,18.42
c10.564,0,13.221-9.968,13.221-18.42C44.721,24.049,42.064,14.08,31.5,14.08z M31.5,21.026c0.429,0,0.82,0.066,1.188,0.157
c0.761,0.656,1.133,1.561,0.403,2.823l-7.036,12.93c-0.216-1.636-0.247-3.24-0.247-4.437C25.808,28.777,26.066,21.026,31.5,21.026z
M36.766,26.987c0.373,1.984,0.426,4.056,0.426,5.513c0,3.723-0.258,11.475-5.69,11.475c-0.428,0-0.822-0.045-1.188-0.136
c-0.07-0.021-0.134-0.043-0.202-0.067c-0.112-0.032-0.23-0.068-0.336-0.11c-1.21-0.515-1.972-1.446-0.874-3.093L36.766,26.987z"/>
<path id="path2815_1_" d="M31.433,0.5c-8.877,0-16.359,3.09-22.454,9.3c-3.087,3.087-5.443,6.607-7.082,10.532
C0.297,24.219-0.5,28.271-0.5,32.5c0,4.268,0.797,8.32,2.397,12.168c1.6,3.85,3.921,7.312,6.969,10.396
c3.085,3.049,6.549,5.399,10.398,7.037c3.886,1.602,7.939,2.398,12.169,2.398c4.229,0,8.34-0.826,12.303-2.465
c3.962-1.639,7.496-3.994,10.621-7.081c3.011-2.933,5.289-6.297,6.812-10.106C62.73,41,63.5,36.883,63.5,32.5
c0-4.343-0.77-8.454-2.33-12.303c-1.562-3.885-3.848-7.32-6.857-10.33C48.025,3.619,40.385,0.5,31.433,0.5z M31.567,6.259
c7.238,0,13.412,2.566,18.554,7.709c2.477,2.477,4.375,5.31,5.67,8.471c1.296,3.162,1.949,6.518,1.949,10.061
c0,7.354-2.516,13.454-7.506,18.33c-2.592,2.516-5.502,4.447-8.74,5.781c-3.2,1.334-6.498,1.994-9.927,1.994
c-3.468,0-6.788-0.653-9.949-1.948c-3.163-1.334-6.001-3.238-8.516-5.716c-2.515-2.514-4.455-5.353-5.826-8.516
c-1.333-3.199-2.017-6.498-2.017-9.927c0-3.467,0.684-6.787,2.017-9.949c1.371-3.2,3.312-6.074,5.826-8.628
C18.092,8.818,24.252,6.259,31.567,6.259z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

View File

@@ -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 ||

View 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 &ndash; 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>

View File

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

View 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>

View File

@@ -1,7 +1,7 @@
<template> <template>
<!-- QUICK NAV --> <!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50"> <nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2"> <ul class="flex text-2xl p-2 gap-2 max-w-3xl mx-auto">
<!-- Home Feed --> <!-- Home Feed -->
<li <li
:class="{ :class="{

View File

@@ -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;

View File

@@ -1,11 +1,12 @@
export interface Contact { export interface Contact {
did: string; did: string;
name?: string; name?: string;
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
publicKeyBase64?: string; publicKeyBase64?: string;
seesMe?: boolean; seesMe?: boolean;
registered?: boolean; registered?: boolean;
} }
export const ContactSchema = { export const ContactSchema = {
contacts: "&did, name, publicKeyBase64, registered, seesMe", contacts: "&did, name", // no need to key by other things
}; };

View File

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

View File

@@ -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.
*/ */

View File

@@ -173,3 +173,19 @@ export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
return jwt.payload; return jwt.payload;
}; };
export const nextDerivationPath = (origDerivPath: string) => {
let lastStr = origDerivPath.split("/").slice(-1)[0];
if (lastStr.endsWith("'")) {
lastStr = lastStr.slice(0, -1);
}
const lastNum = parseInt(lastStr, 10);
const newLastNum = lastNum + 1;
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : "");
const newDerivPath = origDerivPath
.split("/")
.slice(0, -1)
.concat([newLastStr])
.join("/");
return newDerivPath;
};

View File

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

View File

@@ -1,5 +1,266 @@
// many of these are also found in endorser-mobile utility.ts // many of these are also found in endorser-mobile utility.ts
import axios, { AxiosResponse } from "axios";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
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
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 (
subscription: PushSubscription,
skipFilter: boolean,
): Promise<AxiosResponse> => {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
const auth = Buffer.from(subscription.getKey("auth"));
const authB64 = auth
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const p256dh = Buffer.from(subscription.getKey("p256dh"));
const p256dhB64 = p256dh
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const newPayload = {
endpoint: subscription.endpoint,
keys: {
auth: authB64,
p256dh: p256dhB64,
},
message: `Test, where you will see this message ${
skipFilter ? "un" : ""
}filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
};
console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload);
const response = await axios.post(
pushUrl + "/web-push/send-test",
payloadStr,
{
headers: {
"Content-Type": "application/json",
},
},
);
console.log("Got response from web push server:", response);
return response;
};

View File

@@ -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,
@@ -56,6 +60,8 @@ import {
faSpinner, faSpinner,
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus,
faStar,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
faUser, faUser,
@@ -89,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,
@@ -109,6 +119,8 @@ library.add(
faSpinner, faSpinner,
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus,
faStar,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
faUser, faUser,

View File

@@ -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" +

View File

@@ -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",

View File

@@ -3,7 +3,17 @@
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <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,42 +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 }}
</h2>
<span v-else>
<router-link
:to="{ name: 'new-edit-account' }"
class="block w-full text-center text-md text-slate-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
>
(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>
<router-link
:to="{ name: 'new-edit-account' }"
class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
>
Edit Identity
</router-link>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<div <div
v-if="!notificationMaybeChanged" v-if="!notificationMaybeChanged"
@@ -130,46 +137,24 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
Notification status may have changed. Revisit this page to see the Notification status may have changed. Refresh this page to see the
latest setting. latest setting.
</div> </div>
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications"> <router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
Test your notification setup. Troubleshoot your notification setup.
</router-link> </router-link>
</div> </div>
<h3 class="text-sm uppercase font-semibold mb-3">Data</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>
<a
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)
</a>
<a ref="downloadLink" />
<div v-if="activeDid" class="my-8">
<h3 class="text-sm uppercase font-semibold mb-3">Rate Limits</h3>
<button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
@click="checkLimits()"
>
Check Limits
</button>
<!-- show spinner if loading limits --> <!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center mb-4"> <div v-if="loadingLimits" class="text-center">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa> Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
</div> </div>
<div class="mb-4"> <div>
{{ limitsMessage }} {{ limitsMessage }}
</div> </div>
<div v-if="!!limits?.nextWeekBeginDateTime"> <div v-if="!!limits?.nextWeekBeginDateTime">
@@ -195,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 -->
@@ -212,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
@@ -229,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>
@@ -245,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>
@@ -264,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 -->
@@ -293,100 +322,86 @@
</div> </div>
</label> </label>
<router-link <div>
:to="{ name: 'statistics' }" <h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" <div class="px-4 py-4">
> <input
See Animated Global History of Giving type="text"
</router-link> class="block w-full rounded border border-slate-400 px-4 py-2"
v-model="apiServerInput"
/>
<button
v-if="apiServerInput != apiServer"
class="w-full px-4 rounded bg-yellow-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>
<!-- id used by puppeteer test script --> <label
<router-link for="toggleProdWarningMessage"
id="switch-identity-link" class="flex items-center justify-between cursor-pointer px-4 py-4"
:to="{ name: 'identity-switcher' }" @click="toggleProdWarning"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" >
> <!-- label -->
Switch Identity <h2>Show warning if on prod server</h2>
</router-link> <!-- 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>
<div class="flex py-4"> <label
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2> for="toggleTestWarningMessage"
<input class="flex items-center justify-between cursor-pointer px-4 py-4"
type="text" @click="toggleTestWarning"
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> <!-- label -->
</button> <h2>Show warning if on non-prod server</h2>
<button <!-- toggle -->
class="px-3 rounded bg-slate-200 border border-slate-400" <div class="relative ml-2">
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER" <!-- input -->
> <input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
Use Prod <!-- line -->
</button> <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<button <!-- dot -->
class="px-3 rounded bg-slate-200 border border-slate-400" <div
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
> ></div>
Use Test </div>
</button> </label>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
>
Use Local
</button>
</div> </div>
<label <h2 class="text-slate-500 text-sm font-bold mb-2">
for="toggleProdWarningMessage" Notification Push Server
class="flex items-center justify-between cursor-pointer my-4" </h2>
@click="toggleProdWarning" <div class="px-3 py-4">
>
<!-- 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"
@@ -394,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>
@@ -421,21 +436,56 @@
<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>
<script lang="ts"> <script lang="ts">
import { AxiosError, AxiosRequestConfig } from "axios"; import { AxiosError, AxiosRequestConfig } from "axios";
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 { 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";
@@ -459,37 +509,37 @@ interface IAccount {
derivationPath: string; derivationPath: string;
} }
const inputFileNameRef = ref<Blob>();
@Component({ components: { QuickNav, TopMessage } }) @Component({ components: { QuickNav, TopMessage } })
export default class AccountViewView extends Vue { 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 = "";
apiServerInput = ""; apiServerInput = "";
derivationPath = ""; derivationPath = "";
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
givenName = ""; givenName = "";
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;
@@ -530,6 +580,12 @@ export default class AccountViewView extends Vue {
} }
} }
beforeUnmount() {
if (this.downloadUrl) {
URL.revokeObjectURL(this.downloadUrl);
}
}
/** /**
* Initializes component state with values from the database or defaults. * Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database. * @param {SettingsType} settings - Object containing settings from the database.
@@ -553,26 +609,17 @@ export default class AccountViewView extends Vue {
try { try {
// Open the accounts database // Open the accounts database
await accountsDB.open(); await accountsDB.open();
} catch (error) {
console.error("Failed to open accounts database:", error);
return null;
}
let account: { identity?: string } | undefined;
try {
// Search for the account with the matching DID (decentralized identifier) // Search for the account with the matching DID (decentralized identifier)
account = await accountsDB.accounts const account: { identity?: string } | undefined =
.where("did") await accountsDB.accounts.where("did").equals(activeDid).first();
.equals(activeDid)
.first(); // Return parsed identity or null if not found
return JSON.parse((account?.identity as string) || "null");
} catch (error) { } catch (error) {
console.error("Failed to find account:", error); console.error("Failed to find account:", error);
return null; return null;
} }
// Return parsed identity or null if not found
return JSON.parse((account?.identity as string) || "null");
} }
/** /**
@@ -628,11 +675,6 @@ export default class AccountViewView extends Vue {
return timeStr.substring(0, timeStr.indexOf("T")); return timeStr.substring(0, timeStr.indexOf("T"));
} }
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
/** /**
* Processes the identity and updates the component's state. * Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information. * @param {IdentityType} identity - Object containing identity information.
@@ -646,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,
@@ -690,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);
} }
} }
@@ -788,13 +829,13 @@ export default class AccountViewView extends Vue {
const blob = await this.generateDatabaseBlob(); const blob = await this.generateDatabaseBlob();
// Create a temporary URL for the blob // Create a temporary URL for the blob
const url = this.createBlobURL(blob); this.downloadUrl = this.createBlobURL(blob);
// Trigger the download // Trigger the download
this.downloadDatabaseBackup(url); this.downloadDatabaseBackup(this.downloadUrl);
// Revoke the temporary URL // Revoke the temporary URL -- not yet because of DuckDuckGo download failure
URL.revokeObjectURL(url); //URL.revokeObjectURL(this.downloadUrl);
// Notify the user that the download has started // Notify the user that the download has started
this.notifyDownloadStarted(); this.notifyDownloadStarted();
@@ -831,7 +872,19 @@ export default class AccountViewView extends Vue {
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = url; downloadAnchor.href = url;
downloadAnchor.download = `${db.name}-backup.json`; downloadAnchor.download = `${db.name}-backup.json`;
downloadAnchor.click(); downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo
}
public computedStartDownloadLinkClassNames() {
return {
invisible: this.downloadUrl,
};
}
public computedDownloadLinkClassNames() {
return {
invisible: !this.downloadUrl,
};
} }
/** /**
@@ -843,7 +896,7 @@ export default class AccountViewView extends Vue {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Download Started", title: "Download Started",
text: "See your downloads directory for the backup.", text: "See your downloads directory for the backup. It is in the Dexie format.",
}, },
-1, -1,
); );
@@ -867,10 +920,62 @@ export default class AccountViewView extends Vue {
console.error("Export Error:", error); console.error("Export Error:", error);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async uploadFile(event: any) {
inputFileNameRef.value = event.target.files[0];
}
showContactImport() {
return !!inputFileNameRef.value;
}
/**
* Asynchronously imports the database from a downloadable JSON file.
*
* @throws Will notify the user if there is an export error.
*/
async submitFile() {
if (inputFileNameRef.value != null) {
if (
confirm(
"This will replace all settings and contacts, so we recommend you first do the backup step above." +
" Are you sure you want to import and replace all contacts and settings?",
)
) {
await db.delete();
await Dexie.import(inputFileNameRef.value, {
progressCallback: this.progressCallback,
});
}
}
}
private progressCallback(progress: ImportProgress) {
console.log(
`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;
}
async checkLimits() { async checkLimits() {
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.";
} }
} }
@@ -911,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;
@@ -939,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);
} }
} }
@@ -996,6 +1107,7 @@ export default class AccountViewView extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = accounts[accountNum - 1]; const account = accounts[accountNum - 1];
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did }); await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
this.updateActiveAccountProperties(account); this.updateActiveAccountProperties(account);

View File

@@ -1,7 +1,7 @@
<template> <template>
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
@@ -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,30 +246,124 @@
</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">
<pre class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"> {{ serverUtil.containsHiddenDid(veriClaim) ? "Visible " : "" }}Details
{{ util.inspect(veriClaim, false, null) }} </h2>
</pre> <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
>&nbsp;<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
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
>{{ veriClaimDump }}</pre
>
</div> </div>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2> <h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2>
@@ -154,13 +378,13 @@
<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>
</div> </div>
<div v-else> <div v-else>
<pre>{{ util.inspect(fullClaim, false, null) }}</pre> <pre>{{ fullClaimDump }}</pre>
</div> </div>
<a <a
@@ -175,10 +399,11 @@
<script lang="ts"> <script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios"; import { AxiosError, RawAxiosRequestHeaders } from "axios";
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";
@@ -187,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;
@@ -204,20 +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 = "";
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 = "";
veriClaimDidsVisible = {};
windowLocation = window.location.href;
util = util; R = R;
containsHiddenDid = serverUtil.containsHiddenDid; yaml = yaml;
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();
@@ -231,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(
{ {
@@ -249,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() {
@@ -269,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;
@@ -287,57 +554,83 @@ 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 {
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.veriClaim = resp.data; this.veriClaim = resp.data;
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,
}); });
@@ -363,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");
@@ -384,9 +684,10 @@ export default class ClaimView extends Vue {
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.fullClaim = resp.data; this.fullClaim = resp.data;
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",
@@ -422,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
@@ -459,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",
@@ -472,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>

View File

@@ -1,6 +1,6 @@
<template> <template>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">

View File

@@ -1,6 +1,6 @@
<template> <template>
<QuickNav selected="Contacts"></QuickNav> <QuickNav selected="Contacts"></QuickNav>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<h1 <h1
@@ -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,
); );

View File

@@ -1,7 +1,7 @@
<template> <template>
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
@@ -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);
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<!-- Back --> <!-- Back -->
@@ -41,33 +41,45 @@
: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>
<script lang="ts"> <script lang="ts">
import * as didJwt from "did-jwt";
import { sha256 } from "ethereum-cryptography/sha256.js";
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader"; import { QrcodeStream } from "vue-qrcode-reader";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
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 * as R from "ramda"; import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto";
import { SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { import {
@@ -111,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;
@@ -131,6 +143,14 @@ export default class ContactQRScanShow extends Vue {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex; const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const newDerivPath = nextDerivationPath(account.derivationPath);
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
const nextPublicEncKeyHashBase64 =
Buffer.from(nextPublicEncKeyHash).toString("base64");
const contactInfo = { const contactInfo = {
iat: Date.now(), iat: Date.now(),
iss: this.activeDid, iss: this.activeDid,
@@ -139,6 +159,7 @@ export default class ContactQRScanShow extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
publicEncKey, publicEncKey,
nextPublicEncKeyHash: nextPublicEncKeyHashBase64,
}, },
}; };

View File

@@ -1,5 +1,5 @@
<template> <template>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">

View File

@@ -1,6 +1,6 @@
<template> <template>
<QuickNav selected="Contacts"></QuickNav> <QuickNav selected="Contacts"></QuickNav>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Contacts Your Contacts
@@ -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 (base 16 or 64)" 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,16 +110,34 @@
<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>
<div class="text-sm truncate" v-if="contact.nextPubKeyHashB64">
Next Public Key Hash (base 64):
{{ contact.nextPubKeyHashB64 }}
</div>
<div id="ContactActions" class="flex gap-1.5 mt-2"> <div id="ContactActions" class="flex gap-1.5 mt-2">
<div v-if="activeDid"> <div v-if="activeDid">
<button <button
v-if="contact.seesMe" v-if="contact.seesMe"
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="setVisibility(contact, false, true)" @click="setVisibility(contact, false, true)"
title="They can see you" title="They can see you"
> >
@@ -122,14 +145,14 @@
</button> </button>
<button <button
v-else v-else
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true, true)" @click="setVisibility(contact, true, true)"
title="They cannot see you" title="They cannot see you"
> >
<fa icon="eye-slash" class="fa-fw" /> <fa icon="eye-slash" class="fa-fw" />
</button> </button>
<button <button
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)" @click="checkVisibility(contact)"
title="Check Visibility" title="Check Visibility"
v-if="activeDid" v-if="activeDid"
@@ -140,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>
@@ -212,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>
@@ -222,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">
@@ -232,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>
@@ -254,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";
@@ -266,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;
@@ -308,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();
@@ -330,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 = "";
} }
@@ -344,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;
@@ -473,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(
@@ -488,12 +541,51 @@ 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;
} }
let did = this.contactInput; let did = this.contactInput;
let name, publicKeyBase64; let name, publicKeyInput, nextPublicKeyHashInput;
const commaPos1 = this.contactInput.indexOf(","); const commaPos1 = this.contactInput.indexOf(",");
if (commaPos1 > -1) { if (commaPos1 > -1) {
did = this.contactInput.substring(0, commaPos1).trim(); did = this.contactInput.substring(0, commaPos1).trim();
@@ -501,19 +593,76 @@ export default class ContactsView extends Vue {
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1); const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
if (commaPos2 > -1) { if (commaPos2 > -1) {
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim(); name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim(); publicKeyInput = this.contactInput.substring(commaPos2 + 1).trim();
const commaPos3 = this.contactInput.indexOf(",", commaPos2 + 1);
if (commaPos3 > -1) {
publicKeyInput = this.contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
nextPublicKeyHashInput = this.contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
}
} }
} }
// help with potential mistakes while this sharing requires copy-and-paste // help with potential mistakes while this sharing requires copy-and-paste
let publicKeyBase64 = publicKeyInput;
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) { if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
// it must be all hex (compressed public key), so convert // it must be all hex (compressed public key), so convert
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64"); publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
} }
const newContact = { did, name, publicKeyBase64 }; let nextPubKeyHashB64 = nextPublicKeyHashInput;
if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
// it must be all hex (compressed public key), so convert
nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier
}
const newContact = {
did,
name,
publicKeyBase64,
nextPubKeyHashB64: nextPubKeyHashB64,
};
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(
@@ -530,6 +679,7 @@ export default class ContactsView extends Vue {
return this.addContact({ return this.addContact({
did: payload.iss, did: payload.iss,
name: payload.own.name, name: payload.own.name,
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
publicKeyBase64: payload.own.publicEncKey, publicKeyBase64: payload.own.publicEncKey,
} as Contact); } as Contact);
} }
@@ -548,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)
@@ -561,10 +723,20 @@ 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) {
this.$notify(
{
group: "alert",
type: "info",
title: "New User?",
text: "If they are a new user, be sure to register to onboard them.",
},
-1,
);
} }
this.$notify( this.$notify(
{ {
@@ -573,32 +745,28 @@ export default class ContactsView extends Vue {
title: "Contact Added", title: "Contact Added",
text: addedMessage, text: addedMessage,
}, },
5000, -1, // keeping it up so that the "visibility" message is seen
); );
if (this.isRegistered) {
// putting this last so that it shows on the top
this.$notify(
{
group: "alert",
type: "info",
title: "New User?",
text:
"If " +
newContact.name +
" is a new user, be sure to register them.",
},
-1,
);
}
}) })
.catch((err) => { .catch((err) => {
console.error("Error when adding contact to storage:", err); console.error("Error when adding contact to storage:", err);
let message = "An error prevented this import.";
if (
err.message?.indexOf("Key already exists in the object store.") > -1
) {
message =
"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",
type: "danger", type: "danger",
title: "Contact Not Added", title: "Contact Not Added",
text: "An error prevented this import.", text: message,
}, },
-1, -1,
); );
@@ -626,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"
@@ -702,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,
); );
@@ -742,63 +912,70 @@ export default class ContactsView extends Vue {
visibility: boolean, visibility: boolean,
showSuccessAlert: boolean, showSuccessAlert: boolean,
) { ) {
const url = const visibilityPrompt =
this.apiServer + showSuccessAlert &&
"/api/report/" + (visibility
(visibility ? "canSeeMe" : "cannotSeeMe"); ? "Are you sure you want to make your activity visible to them?"
const identity = await this.getIdentity(this.activeDid); : "Are you sure you want to hide all your activity from them?");
const headers = await this.getHeaders(identity); if (!visibilityPrompt || confirm(visibilityPrompt)) {
const payload = JSON.stringify({ did: contact.did }); const url =
this.apiServer +
"/api/report/" +
(visibility ? "canSeeMe" : "cannotSeeMe");
const identity = await this.getIdentity(this.activeDid);
const headers = await this.getHeaders(identity);
const payload = JSON.stringify({ did: contact.did });
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
if (resp.status === 200) { if (resp.status === 200) {
if (showSuccessAlert) { if (showSuccessAlert) {
this.$notify(
{
group: "alert",
type: "success",
title: "Visibility Set",
text:
this.nameForDid(this.contacts, contact.did) +
" can " +
(visibility ? "" : "not ") +
"see your activity.",
},
-1,
);
}
contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility });
} else {
console.error(
"Got some bad server response when setting visibility: ",
resp.status,
resp,
);
const message =
resp.data.error?.message || "Got some error setting visibility.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "danger",
title: "Visibility Set", title: "Error Setting Visibility",
text: text: message,
this.nameForDid(this.contacts, contact.did) +
" can " +
(visibility ? "" : "not ") +
"see your activity.",
}, },
-1, -1,
); );
} }
contact.seesMe = visibility; } catch (err) {
db.contacts.update(contact.did, { seesMe: visibility }); console.error("Got some error when setting visibility:", err);
} else {
console.error(
"Got some bad server response when setting visibility: ",
resp.status,
resp,
);
const message =
resp.data.error?.message || "Got some error setting visibility.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error Setting Visibility", title: "Error Setting Visibility",
text: message, text: "Check connectivity and try again.",
}, },
-1, -1,
); );
} }
} catch (err) {
console.error("Got some error when setting visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Visibility",
text: "Check connectivity and try again.",
},
-1,
);
} }
} }
@@ -869,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> {
@@ -914,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,
); );
@@ -924,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,
); );
@@ -954,7 +1131,7 @@ export default class ContactsView extends Vue {
"?", "?",
) )
) { ) {
this.createAndSubmitGive( this.createAndSubmitContactGive(
identity, identity,
fromDid, fromDid,
toDid, toDid,
@@ -966,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,
@@ -1035,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) {

View File

@@ -1,9 +1,9 @@
<template> <template>
<QuickNav selected="Discover"></QuickNav> <QuickNav selected="Discover" />
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- 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">
Discover Discover
@@ -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,
}; };
} }
} }

View File

@@ -2,7 +2,7 @@
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<!-- Back --> <!-- Back -->
@@ -21,10 +21,11 @@
</h1> </h1>
</div> </div>
<!-- eslint-disable prettier/prettier -->
<div> <div>
<p>Here are ways to test notifications and get them working.</p> <p>Here are ways to test notifications and get them working.</p>
<h2 class="text-xl font-semibold">Full Test</h2> <h2 class="text-xl font-semibold mt-4">Full Test</h2>
<div> <div>
<p> <p>
If this works then you're all set. If this works then you're all set.
@@ -38,13 +39,16 @@
</p> </p>
</div> </div>
<h2 class="text-xl font-semibold">If this app is not installed...</h2> <h2 class="text-xl font-semibold mt-4">
If this app doesn't support notifications...
<!-- Note that that exact verbiage shows in a message elsewhere. -->
</h2>
<div> <div>
<p> <p>
For best results on mobile, install this app on your device (as To be notified of interesting updates, install this app on your device
opposed to using it inside the broser app). In Chrome, it may prompt (as opposed to using it inside the browser app). In Chrome, it may prompt
you, and you can also look for the "Install" command in the browser you, and you can also look for the "Install" command in the browser
settings; on the the deskop, look for this icon in the address bar: settings; on the the desktop, look for this icon in the address bar:
<img <img
src="../assets/help/chrome-install-pwa.png" src="../assets/help/chrome-install-pwa.png"
alt="Chrome 'install' icon" alt="Chrome 'install' icon"
@@ -53,19 +57,31 @@
</p> </p>
</div> </div>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold mt-4">
If "you must enable notifications"... If you must enable notifications...
<!-- Note that that exact verbiage shows in a message elsewhere. -->
</h2> </h2>
<div> <div>
<p> <p>
Wait for about 10 seconds (for the service worker to activate), then
<button class="text-blue-500" @click="showNotificationChoice()"> <button class="text-blue-500" @click="showNotificationChoice()">
click here. Click here.
</button> </button>
</p> </p>
</div> </div>
<h2 class="text-xl font-semibold">Check App Permissions</h2> <h2 class="text-xl font-semibold mt-4">
If you're waiting for system initialization...
<!-- Note that that exact verbiage shows in a message elsewhere. -->
</h2>
<div>
<p>
... and it never stops, then there is a problem with the underlying
service worker or push server mechanism in your browser. Your best bet
is to follow the "Reinstall" steps below or use a different browser.
</p>
</div>
<h2 class="text-xl font-semibold mt-4">Check App Permissions</h2>
<div> <div>
<p> <p>
In Apple iOS, check "Settings" -> "Notifications", look for the Time In Apple iOS, check "Settings" -> "Notifications", look for the Time
@@ -89,8 +105,8 @@
</li> </li>
<li class="list-disc ml-4"> <li class="list-disc ml-4">
Go to Chrome "Settings", then "Privacy and Security" and "Clear Go to Chrome "Settings", then "Privacy and Security" and "Clear
"Clear browsing data", then "Cookies and site data". Also make sure "Clear browsing data", then "Cookies and site data". Make sure the
the "Time Range" at the top shows "All time". "Time Range" at the top shows "All time".
</li> </li>
</ul> </ul>
<p> <p>
@@ -103,7 +119,7 @@
</p> </p>
</div> </div>
<h2 class="text-xl font-semibold">Check Browser Permissions</h2> <h2 class="text-xl font-semibold mt-4">Check Browser Permissions</h2>
<div> <div>
<p>In Apple iOS, check Settings -> Notifications.</p> <p>In Apple iOS, check Settings -> Notifications.</p>
<p>In Android, check Settings -> Notifications.</p> <p>In Android, check Settings -> Notifications.</p>
@@ -118,7 +134,9 @@
</a> </a>
</div> </div>
<h2 class="text-xl font-semibold">Check OS Permissions</h2> <h2 class="text-xl font-semibold mt-4">
Check Operating System (OS) Permissions
</h2>
<div class="px-2"> <div class="px-2">
<div> <div>
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3> <h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
@@ -136,13 +154,15 @@
<h3 class="text-lg font-semibold">Desktop - Mac</h3> <h3 class="text-lg font-semibold">Desktop - Mac</h3>
<div> <div>
<span> <span>
Requires Mac OS 13; see your macOS version under Apple -> About See "System Settings" -> "Notifications" and make sure it is
This Mac. enabled for the browser you're using. Note that these
notifications require Mac OS 13; see your macOS version under
Apple -> "About This Mac".
</span> </span>
</div> </div>
<h3 class="text-lg font-semibold">Windows desktop</h3> <h3 class="text-lg font-semibold">Desktop - Windows</h3>
In Windows, check Settings -> Notifications. In Windows, check "Settings" -> "Notifications".
<img <img
src="../assets/help/windows-system-enable-notifications.png" src="../assets/help/windows-system-enable-notifications.png"
alt="Windows system settings" alt="Windows system settings"
@@ -161,80 +181,120 @@
</div> </div>
</div> </div>
<h2 class="text-xl font-semibold">Reinstall</h2> <h2 class="text-xl font-semibold mt-4">Reinstall</h2>
<div> <div>
<p> <p>
If all else fails, uninstall the app, ensure all the browser tabs with If all else fails, uninstall the app, ensure all the browser tabs with
it are closed, and clear out caches and storage. it are closed, and clear out caches and storage.
</p> </p>
<p>
Of course, you'll want to back up all your data first -- all seeds as
well as the contacts & settings -- on the Account
<fa icon="circle-user" /> page.
</p>
<ul class="ml-4 list-disc"> <ul class="ml-4 list-disc">
<li> <li>
Clear cache for site. (In Chrome, go to `chrome://settings/cookies` Clear cache.
and "all site data and permissions"; in Firefox, go to <ul>
`about:preferences` and search for "cache" then "Manage Data", and <li>
also manually remove the IndexedDB data if the DBs still show.) In mobile, look for the browser app settings. This is true even
for an installed app: go to the browser which you used to
initially visit timesafari.app, because those settings affect
the app. Look for "Delete browsing data" in the "Settings",
under "Privacy and Security".
</li>
<li>
In Chrome, go to `chrome://settings/cookies` and "all site data
and permissions" for timesafari.app; in Firefox, go to
`about:preferences` and search for "cache" then "Manage Data"
for timesafari.app. Also manually remove the IndexedDB data if
the DBs still show.)
</li>
</ul>
</li> </li>
<li> <li>
Clear notification permission. (in Chrome, go to Clear notification permission. (In Chrome, go to
`chrome://settings/content/notifications`; in Firefox, go to `chrome://settings/content/notifications`; in Firefox, go to
`about:preferences` and search for "notifications".) `about:preferences` and search for "notifications".)
</li> </li>
<li> <li>
Unregister service worker. (in Chrome, go to Unregister service worker. (In Chrome, go to
`chrome://serviceworker-internals/`; in Firefox, go to `chrome://serviceworker-internals/`; in Firefox, go to
`about:serviceworkers`.) `about:serviceworkers`.)
</li> </li>
<li> <li>
Clear Cache Storage. (in Chrome, in dev tools under Application; in Clear "Cache Storage". (In Chrome, in dev tools under "Application";
Firefox, in dev tools under Storage.) in Firefox, in dev tools under "Storage".)
</li> </li>
</ul> </ul>
<p>Then reinstall the app.</p> <p>Then reinstall the app.</p>
</div> </div>
<h2 class="text-xl font-semibold mt-4">Tests</h2>
<button <button
@click="showTestNotification()" @click="showTestNotification()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Test Notification Directly to Device (Not Through Push Server) Send Test Notification Directly to Device (Not Through Push Server)
</button> </button>
<p>
If that didn't show a notification on your device, the problem is that
your browser or your operating system are not allowing notifications
through. See "Check App Permissions" and "Check Browser Permissions" and
"Check Operating System (OS) Permissions" above.
</p>
<button <button
@click="alertWebPushSubscription()" @click="alertWebPushSubscription()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Show Web Push Subscription Info Show Web Push Subscription Info
</button> </button>
<p>
If that showed "null" then the notification is not active.
<button class="text-blue-500" @click="showNotificationChoice()">
Click here.
</button>
</p>
<button <button
@click="sendTestWebPushMessage(true)" @click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Yourself a Test Web Push Message (Through Push Server but Skipping Send Yourself a Test Web Push Message (Through Push Server but Skipping
Client Filter) Client Filter)
</button> </button>
<p>
If that didn't show a notification on your device, there is a problem
getting to the push server. Disable notifications and then enable them
again.
</p>
<button <button
@click="sendTestWebPushMessage()" @click="sendTestWebPushMessage()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Yourself a Test Web Push Message (Through Push Server and Client Send Yourself a Test Web Push Message (Through Push Server and Client
Filter) Filter)
</button> </button>
<p>
If you don't see a message, it could be that there is nothing new for
you to see. If the previous test worked, then things should work fine.
If you notice a full 24 hours where you get no notification and you know
that there are new items that should show, gather as many details as
possible and go to the bottom of
<router-link to="help" class="text-blue-500"> this page </router-link>
for ways to contact us.
</p>
</div> </div>
<!-- eslint-enable -->
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { db } from "@/db/index"; import { sendTestThroughPushServer } from "@/libs/util";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface Notification { interface Notification {
group: string; group: string;
@@ -273,6 +333,7 @@ export default class HelpNotificationsView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Not Subscribed", title: "Not Subscribed",
// Note that this exact verbiage shows in help text.
text: "You must enable notifications before testing the web push.", text: "You must enable notifications before testing the web push.",
}, },
-1, -1,
@@ -281,61 +342,16 @@ export default class HelpNotificationsView extends Vue {
} }
try { try {
await db.open(); await sendTestThroughPushServer(this.subscription, skipFilter);
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
const auth = Buffer.from(this.subscription.getKey("auth"));
const authB64 = auth
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const p256dh = Buffer.from(this.subscription.getKey("p256dh"));
const p256dhB64 = p256dh
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const newPayload = {
endpoint: this.subscription.endpoint,
keys: {
auth: authB64,
p256dh: p256dhB64,
},
message: `Test, where you will see this message ${
skipFilter ? "un" : ""
}filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
};
console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload);
const response = await axios.post(
pushUrl + "/web-push/send-test",
payloadStr,
{
headers: {
"Content-Type": "application/json",
},
},
);
console.log("Got response from web push server:", response);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Test Web Push Sent", title: "Test Web Push Sent",
text: "Check your device for the test web push message, depending on the filtering you chose.", text:
"Check your device for the test web push message" +
(skipFilter ? "." : " if there are new items in your feed."),
}, },
-1, -1,
); );

View File

@@ -2,7 +2,7 @@
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<!-- Back --> <!-- Back -->
@@ -21,13 +21,14 @@
</h1> </h1>
</div> </div>
<!-- eslint-disable prettier/prettier -->
<div> <div>
<p> <p>
This app is a window into data that you and your friends own, focused on This app is a window into data that you and your friends own, focused on
gifts and collaboration. gifts and collaboration.
</p> </p>
<h2 class="text-xl font-semibold">What is the philosophy here?</h2> <h2 class="text-xl font-semibold">What is the idea here?</h2>
<p> <p>
We are building networks of people who want to grow a giving society. We are building networks of people who want to grow a giving society.
First of all, you can see what people have given, and also recognize First of all, you can see what people have given, and also recognize
@@ -63,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>
@@ -94,7 +108,7 @@
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup my identifier (secret) data? How do I backup my identifier (secret) data?
</h2> </h2>
<ul class="list-disc list-inside"> <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.
</li> </li>
@@ -110,7 +124,7 @@
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup my other (non-identifier-secret) data? How do I backup my other (non-identifier-secret) data?
</h2> </h2>
<ul class="list-disc list-inside"> <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.
</li> </li>
@@ -124,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>
@@ -132,7 +146,7 @@
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I restore my identifier (secret) data? How do I restore my identifier (secret) data?
</h2> </h2>
<ul class="list-disc list-inside"> <ul class="list-disc list-outside ml-4">
<li> <li>
<router-link class="text-blue-500" to="/import-account"> <router-link class="text-blue-500" to="/import-account">
Go to the import page Go to the import page
@@ -144,12 +158,10 @@
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I restore my other (non-identifier-secret) data? How do I restore my other (non-identifier-secret) data?
</h2> </h2>
<ul class="list-disc list-inside"> <ul class="list-disc list-outside ml-4">
<li> <li>
Make sure you have your backup file (above), then contact us with Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
your interest. This is functionality that has to be written, and click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
your interest will help us prioritize it, but there are also manual
ways to restore your data.
</li> </li>
</ul> </ul>
</div> </div>
@@ -164,14 +176,55 @@
</router-link> </router-link>
</p> </p>
<h2 class="text-xl font-semibold">How do I erase my data?</h2>
<p>
Before doing this, note the two kinds of data to backup: identity data,
and other data for contacts and settings (see instructions above).
</p>
<ul>
<li class="list-disc list-outside ml-4">
Mobile
<ul>
<li class="list-disc list-outside ml-4">
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
</li>
<li class="list-disc list-outside ml-4">
DuckDuckGo: long hold -> Clear Data (takes effect immediately)
</li>
</ul>
</li>
<li class="list-disc list-outside ml-4">
Desktop
<ul>
<li class="list-disc list-outside ml-4">
Chrome:
<a href="chrome://settings/content/all" class="text-blue-500"
>clear here</a
>
also clear under dev tools Application
</li>
<li class="list-disc list-outside ml-4">
Firefox: <a href="about:preferences">go here</a>, Manage Data,
find timesafari.app and select, hit Remove Selected, then Save
Changes
</li>
<li class="list-disc list-outside ml-4">
Safari: Settings -> Privacy -> Manage Website Data, search for
timesafari.app and select, hit Remove Selected, then Done.
</li>
</ul>
</li>
</ul>
<p>To erase your data from our servers, contact us (below).</p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info? I know there is a record from someone, so why can't I see that info?
</h2> </h2>
<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>
@@ -191,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>
@@ -206,11 +266,31 @@
</a> </a>
</p> </p>
<h2 class="text-xl font-semibold">What is your privacy policy?</h2> <h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
<p> <p style="display:inline; align-items: center">
See This work is marked with
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
<span class="text-blue-500 mr-1">CC0 1.0</span>
<img
src="../assets/help/creative-commons-circle.svg"
alt="CC circle"
width="20"
class="display: inline"
/>
<img
src="../assets/help/creative-commons-zero.svg"
alt="CC zero"
width="20"
style="display: inline"
/>
</a>
<br />
For notifications, this service stores push token data; that can be revoked at any time
by disabling notifications on the Account <fa icon="circle-user" class="fa-fw" /> page.
<br />
For all other claim data,
<a href="https://endorser.ch/privacy-policy" class="text-blue-500"> <a href="https://endorser.ch/privacy-policy" class="text-blue-500">
the Endorser Service Privacy Policy. the Endorser Service has this Privacy Policy.
</a> </a>
</p> </p>
@@ -231,16 +311,21 @@
</h2> </h2>
<p> <p>
Contact us at Contact us at
<a mailto="info@TimeSafari.app">info@TimeSafari.app</a> <a href="mailto:info@TimeSafari.app" class="text-blue-500"
>info@TimeSafari.app</a
>
</p> </p>
</div> </div>
<!-- eslint enable -->
</section> </section>
</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;
@@ -262,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,
); );

View File

@@ -3,40 +3,82 @@
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<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">
Time Safari Time Safari
</h1> </h1>
<!-- show the actions for recognizing a give --> <!-- prompt to install notifications -->
<div class="mb-8"> <div class="mb-8">
<div <div
v-if="!isInstalled()" v-if="!notificationsSupported()"
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> <p style="display: inline; align-items: center">
You should install this as an app. This currently doesn't support notifications, so let's fix that.
<router-link <br />
:to="{ name: 'help-notifications' }" <!-- Note that that exact verbiage shows in the help. -->
class="text-blue-500"
<span v-if="userAgentInfo.getOS().name === 'iOS'">
Tap on "Share"<img
src="../assets/help/apple-share-icon.svg"
alt="Apple 'share' icon"
width="30"
style="display: inline; margin: 0 5px; vertical-align: middle"
/>and then "Add to Home Screen"
<fa icon="square-plus" title="Apple 'Add' icon" />
and go click on that new app.
</span>
<span
v-else-if="userAgentInfo.getBrowser().name.startsWith('Chrome')"
> >
Go here for instructions. You should see a prompt to install, or you can click on the
</router-link> top-right dots
<fa
icon="ellipsis-vertical"
title="vertical ellipsis"
class="fa-fw"
/>
and then "Install"<img
src="../assets/help/install-android-chrome.png"
alt="Android 'install' icon"
width="30"
style="display: inline; margin: 0 5px; vertical-align: middle"
/>
and go use that app. If you already did these steps, reload this app
so that it is fully detected.
</span>
<span v-else>
Try
<a href="https://www.google.com/chrome/" class="text-blue-500"
>Google Chrome</a
>
or look for a way to install as an app from this browser.
</span>
</p> </p>
</div> </div>
</div>
<!-- show the actions for recognizing a give -->
<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&hellip;
</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>
@@ -44,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"
> >
@@ -78,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)"
> >
@@ -86,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"
> >
@@ -119,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">
@@ -134,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&hellip; <fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</p> </p>
@@ -157,10 +228,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { UAParser } from "ua-parser-js";
import { Component, Vue } from "vue-facing-decorator"; 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";
@@ -175,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;
@@ -186,6 +260,7 @@ interface Notification {
@Component({ @Component({
components: { components: {
GiftedDialog, GiftedDialog,
GiftedPrompts,
QuickNav, QuickNav,
EntityIcon, EntityIcon,
InfiniteScroll, InfiniteScroll,
@@ -199,17 +274,13 @@ 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
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();
@@ -244,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",
@@ -257,23 +336,15 @@ 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,
); );
} }
} }
// from https://benborgers.com/posts/pwa-detect-installed notificationsSupported() {
isInstalled() { return "Notification" in window;
// For iOS
if ("standalone" in window.navigator) return true;
// For Android
if (window.matchMedia("(display-mode: standalone)").matches) return true;
// If neither is true, it's not installed
return false;
} }
public async buildHeaders() { public async buildHeaders() {
@@ -291,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.",
); );
} }
@@ -313,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) {
@@ -344,7 +415,7 @@ export default class HomeView extends Vue {
-1, -1,
); );
}); });
this.isHiddenSpinner = true; this.isFeedLoading = false;
} }
/** /**
@@ -356,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(),
@@ -436,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>

View File

@@ -1,6 +1,6 @@
<template> <template>
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile"></QuickNav>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
@@ -18,16 +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"
<h2 class="text-xl font-semibold mb-0"> >
{{ givenName }} <fa icon="circle-check" class="fa-fw text-red-600 text-xl mr-3"></fa>
</h2> <div class="text-sm text-slate-500">
<div class="text-sm text-slate-500 truncate"> <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 -->
@@ -38,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">
@@ -88,22 +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 givenName = "";
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();
@@ -111,24 +113,15 @@ export default class IdentitySwitcherView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || ""; this.apiServerInput = settings?.apiServer || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
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) {
@@ -154,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" });
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
@@ -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,
);
} }
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
@@ -72,6 +72,7 @@ import {
DEFAULT_ROOT_DERIVATION_PATH, DEFAULT_ROOT_DERIVATION_PATH,
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
nextDerivationPath,
} from "../libs/crypto"; } from "../libs/crypto";
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";
@@ -121,17 +122,7 @@ export default class ImportAccountView extends Vue {
} }
}); });
// increment the last number in that max derivation path // increment the last number in that max derivation path
let lastStr = accountWithMaxDeriv.derivationPath.split("/").slice(-1)[0]; const newDerivPath = nextDerivationPath(accountWithMaxDeriv.derivationPath);
if (lastStr.endsWith("'")) {
lastStr = lastStr.slice(0, -1);
}
const lastNum = parseInt(lastStr, 10);
const newLastNum = lastNum + 1;
const newDerivPath = accountWithMaxDeriv.derivationPath
.split("/")
.slice(0, -1)
.concat([newLastNum.toString() + "'"])
.join("/");
const mne: string = accountWithMaxDeriv.mnemonic; const mne: string = accountWithMaxDeriv.mnemonic;

View File

@@ -1,5 +1,5 @@
<template> <template>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">

View File

@@ -1,67 +0,0 @@
<template>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- 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>

View File

@@ -1,7 +1,7 @@
<template> <template>
<QuickNav selected="Projects"></QuickNav> <QuickNav selected="Projects"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
@@ -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&hellip;</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);
} }
} }

View File

@@ -2,7 +2,7 @@
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<!-- Back --> <!-- Back -->
@@ -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" });

View File

@@ -3,7 +3,7 @@
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
@@ -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&hellip; 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&hellip;
</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&hellip; Show More Contacts&hellip;
</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>

View File

@@ -2,14 +2,50 @@
<QuickNav selected="Projects"></QuickNav> <QuickNav selected="Projects"></QuickNav>
<TopMessage /> <TopMessage />
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Ideas Your 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>

View File

@@ -2,7 +2,7 @@
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<!-- Back --> <!-- Back -->

View File

@@ -1,7 +1,17 @@
<template> <template>
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <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",

View File

@@ -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>

View File

@@ -2,7 +2,7 @@
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<!-- Back --> <!-- Back -->
@@ -23,7 +23,7 @@
<div> <div>
Here is a view of the activity you can see. Here is a view of the activity you can see.
<ul class="list-disc list-inside"> <ul class="list-disc outside ml-4">
<li>Each identity and claim has a unique position.</li> <li>Each identity and claim has a unique position.</li>
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. --> <!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
<li>Each will show at their time of appearance relative to all others.</li> <li>Each will show at their time of appearance relative to all others.</li>

View File

@@ -2,7 +2,7 @@
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div class="mb-8"> <div class="mb-8">
<!-- Back --> <!-- Back -->

29
sw_combine.js Normal file
View 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.");
});

View File

@@ -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,23 +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("Not logging to DB because self.appendDailyLog doesn't exist."); console.log(
"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) => {
@@ -82,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 = {

View File

@@ -414,11 +414,11 @@ async function appendDailyLog(message) {
const db = await openIndexedDB("TimeSafari"); const db = await openIndexedDB("TimeSafari");
const transaction = db.transaction("logs", "readwrite"); const transaction = db.transaction("logs", "readwrite");
const store = transaction.objectStore("logs"); const store = transaction.objectStore("logs");
// will only keep one day's worth of logs // only keep one day's worth of logs
const todayKey = new Date().toDateString(); const todayKey = new Date().toDateString();
const previous = await getRecord(store, todayKey); const previous = await getRecord(store, todayKey);
if (!previous) { if (!previous) {
await store.clear(); // clear out anything older than today await store.clear(); // clear out everything previous when this is today's first log
} }
let fullMessage = (previous && previous.message) || ""; let fullMessage = (previous && previous.message) || "";
if (fullMessage) { if (fullMessage) {
@@ -468,9 +468,6 @@ async function fetchAllAccounts() {
if (!db.objectStoreNames.contains("accounts")) { if (!db.objectStoreNames.contains("accounts")) {
db.createObjectStore("accounts", { keyPath: "id" }); db.createObjectStore("accounts", { keyPath: "id" });
} }
if (!db.objectStoreNames.contains("worker_log")) {
db.createObjectStore("worker_log");
}
}; };
openRequest.onsuccess = function (event) { openRequest.onsuccess = function (event) {
@@ -550,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);

View File

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