Compare commits

...

153 Commits

Author SHA1 Message Date
d26d1d3601 bump to version 0.1.8 2023-12-27 19:50:47 -07:00
1e6159869f update daily check title & documentation 2023-12-27 18:51:49 -07:00
75d15ddeb9 add note to install as an app 2023-12-27 14:46:10 -07:00
051a0a97d8 fix issuer name in project list 2023-12-27 14:13:05 -07:00
f8d3fe2ee1 enhance service-worker logging, allow for filtered-push test 2023-12-27 13:59:24 -07:00
4f0a046723 fix quick-give check on contact page & add message on detailed view 2023-12-27 13:58:42 -07:00
c4a0458c08 doc: add to notification-help page & task list 2023-12-26 21:43:04 -07:00
25b1598fcb doc: add more help for the notifications 2023-12-26 17:48:14 -07:00
ddbb700c34 Merge pull request 'Daily service-worker logging' (#100) from sw-log into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#100
2023-12-25 14:51:22 -05:00
fd8877900b add another alert message & test button 2023-12-25 12:51:06 -07:00
05c6ddda02 allow a test notification from the notification help screen 2023-12-24 21:24:51 -07:00
853eb3c623 include the data in the logged info for a service worker "push" 2023-12-23 19:34:26 -07:00
44cfe0d88e allow notifications even without an ID 2023-12-22 14:22:13 -07:00
7fe256dc9e log service worker messages to the DB (now works) 2023-12-22 12:51:18 -07:00
e739d0be7c update error messages to be less... confusing 2023-12-22 09:19:36 -07:00
8d873b51bd doc: update tasks 2023-12-21 21:03:47 -07:00
d7f4acb702 make more adjustments to try and get logging to work 2023-12-21 20:50:35 -07:00
f8002c4550 add DB logging for service-worker events 2023-12-20 20:40:00 -07:00
d6b1386741 add console logging for all service worker events 2023-12-20 19:49:04 -07:00
50fdd95c60 increment version & add -beta 2023-12-19 20:41:57 -07:00
91c6c7c11c bump to version 0.1.7 2023-12-19 20:39:11 -07:00
4e28dc8de6 update commentary, help, kudos 2023-12-19 20:35:28 -07:00
fb425f0d51 update all icon images to our own art 2023-12-19 20:29:19 -07:00
a19aebcb37 simplify the notification message logic, hopefully fixing what's on servers 2023-12-18 19:12:09 -07:00
d0697c1ef4 fix top warnings when on prod or non-prod servers 2023-12-18 16:06:55 -07:00
1dd2333624 Merge pull request 'claim-view-improvements' (#99) from claim-view-improvements into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#99
2023-12-18 16:54:02 -05:00
Jose Olarte III
b4b78f6a2c Recolored buttons 2023-12-18 19:46:58 +08:00
Jose Olarte III
3c0f6ce0de Design and uniformity tweaks 2023-12-18 19:44:03 +08:00
5534f8fa50 fix logic for prod & test host detection 2023-12-17 20:17:45 -07:00
a5004d475e bump version to next -beta 2023-12-17 20:02:28 -07:00
b445b1234f bump version to 0.1.6 2023-12-17 20:00:12 -07:00
17c96dd01a fix linting, etc with previous feature (env warning) 2023-12-16 17:17:54 -07:00
6ad17101b2 Merge pull request 'add warning if on unexpected server' (#98) from server-warn into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#98
2023-12-16 10:08:17 -05:00
b4085ffaa7 Merge branch 'master' into server-warn 2023-12-16 08:08:00 -07:00
4f2cb55753 add warning if on unexpected server 2023-12-16 08:04:16 -07:00
ebf9164ecc Merge pull request 'add infinite scroll to the home page feed' (#97) from home-infinite into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#97
2023-12-15 07:49:01 -05:00
540cc21839 add infinite scroll to the home page feed 2023-12-14 21:54:15 -07:00
c182068901 give more details on map-overlap issue 2023-12-13 21:27:13 -07:00
aaa1f31945 fix one single linting problem 2023-12-13 21:16:17 -07:00
17c632eb16 on brand new ID, go back home (plus some task adjustments) 2023-12-13 20:19:29 -07:00
41c4cbe61a fix more messaging and actions if they don't have an ID 2023-12-13 20:05:58 -07:00
c8402797ad remove "mute" notifications (since they can turn them off, and the mute functionality isn't built) 2023-12-13 19:53:46 -07:00
4a09b9b9b1 more fixes for when there is no identity 2023-12-13 19:46:40 -07:00
5db3423301 enhance error messages 2023-12-13 19:17:18 -07:00
2b00b243e8 fix error when lacking ID, and format linked projects 2023-12-13 19:13:04 -07:00
f2e5d8168d Merge pull request 'design-tweaks-2023-12' (#96) from design-tweaks-2023-12 into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#96
2023-12-13 11:20:54 -05:00
1d262b8da9 Merge branch 'master' into design-tweaks-2023-12 2023-12-13 11:17:33 -05:00
8ed74b71f2 change colors and add spacing to make buttons obvious 2023-12-13 09:16:45 -07:00
8fb21c3d89 doc: update tasks, update verbiage, and add to help doc 2023-12-13 08:58:04 -07:00
8dbfcd38d3 fix bad text in pop-up, and fix name of class 2023-12-13 08:57:24 -07:00
04df0d4eff fix problem when there's a null description 2023-12-13 08:56:53 -07:00
Jose Olarte III
ab523639a5 Normalized button visual styles 2023-12-13 18:48:45 +08:00
Jose Olarte III
0484dfb253 Spacing and typography fixes 2023-12-13 18:37:03 +08:00
Jose Olarte III
c2839e8a99 Mobile-style flushed-right toggle switches 2023-12-13 18:22:15 +08:00
Jose Olarte III
e533cd3d34 Visual improvements to "set name" button 2023-12-13 18:16:15 +08:00
Jose Olarte III
18e00b95c7 Fixed size and alignment of QR code button 2023-12-13 17:41:27 +08:00
Jose Olarte III
e97cd1b1fa Minor visual improvements in "giving recognition" section 2023-12-13 17:29:53 +08:00
ccca93b9f1 change some messages, rework tasks 2023-12-11 19:19:29 -07:00
1be6c04699 prompt them to fill in their name when sharing their info 2023-12-11 16:36:50 -07:00
2c33febb0e fix location of web-push unsubscribe action 2023-12-10 20:22:41 -07:00
e6f73dc81c add an unsubscribe to the web push 2023-12-10 20:17:14 -07:00
0d55a722c5 there's no need to capitalize EVERYTHING 2023-12-10 18:55:13 -07:00
97ef78f5dd add better debug logging for web-push info 2023-12-10 18:53:58 -07:00
672abac9a9 show web-push subscription info on demand, and refine docs 2023-12-10 18:43:24 -07:00
0607fad3e5 remove the 'never' option for notifications & close on 'maybe later' 2023-12-10 17:41:03 -07:00
6aa89a1d1d update icon and favicon 2023-12-10 16:53:35 -07:00
2556d5feb9 Merge pull request 'add hash to help page for tracking exact versions' (#95) from git-hash into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#95
2023-12-10 11:21:23 -05:00
3c1654764c add commit hash to help page 2023-12-10 09:20:03 -07:00
4c1e229d62 compute commit hash with git-describe 2023-12-10 09:18:07 -07:00
17444d75de remove references to test file 2023-12-10 08:59:25 -07:00
f2fb432d2e update documentation for going to production 2023-12-09 22:15:24 -07:00
e45689daed bump version to next beta 2023-12-09 21:21:35 -07:00
041308ebc9 fix app name so that it'll build 2023-12-09 20:39:17 -07:00
9c36bb509a bump to v 0.1.5 and other misc tweaks 2023-12-09 20:36:51 -07:00
2c300614ef add claim info link to offer & give lines on a project 2023-12-08 22:31:49 -07:00
8849e8806a Merge pull request 'allow changing the units being given' (#90) from other-units into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#90
2023-12-09 00:22:32 -05:00
f75094283a Merge branch 'master' into other-units 2023-12-09 00:22:20 -05:00
0fabccd410 revert util.d.ts to original (different spacing & capitalization) 2023-12-08 22:04:28 -07:00
Matthew Raymer
8ddf7d9532 Merging sw-cleanup 2023-12-08 23:54:04 -05:00
Matthew Raymer
4078853558 Merge branch 'sw-cleanup' 2023-12-08 23:35:27 -05:00
Matthew Raymer
f4df5ffa9a Merge branch 'master' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa 2023-12-08 23:34:36 -05:00
fa856f7594 Merge pull request 'add ability to view specific details of a claim, and also confirm it' (#91) from claim into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#91
2023-12-08 23:24:41 -05:00
Matthew Raymer
a60beb483c Adding alert dialogs 2023-12-08 23:05:24 -05:00
a0db6433a6 Merge branch 'master' into other-units 2023-12-08 23:05:21 -05:00
59d0772881 ask for confirmation before submitting a confirm claim 2023-12-08 14:13:23 -07:00
b18e554886 add ability to confirm a claim 2023-12-08 14:10:01 -07:00
098ef3c644 add Claim view for details about a specific server record 2023-12-08 11:40:09 -07:00
6045975b79 add more tasks for notifications work 2023-12-07 20:44:48 -07:00
a6bb036ceb fix name of new HelpNotificationsView class 2023-12-07 20:35:29 -07:00
1e2ad85547 add dedicated help page for looking into notifications 2023-12-07 20:33:17 -07:00
Matthew Raymer
3e2723b744 Added auto-control on notification toggle -- be sure to empty browser cache in Storage to assure latest scripts are executing 2023-12-07 04:20:41 -05:00
4daffe8f40 doc: fix note about remaining py_push_server work 2023-12-06 14:35:57 -07:00
efb1922826 Merge pull request 'other-smalls' (#89) from other-smalls into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#89
2023-12-06 16:22:55 -05:00
c6e10bfdad update tasks 2023-12-05 20:03:19 -07:00
bb122be319 add URL for plans 2023-12-05 19:55:44 -07:00
3f436476a2 fix project loading & saving to include all the claim data 2023-12-05 18:47:56 -07:00
a77d20b572 show appropriate icon next to amount numbers (and some docs) 2023-12-05 17:58:46 -07:00
393d1583ae allow changing of units being given 2023-12-05 14:55:00 -07:00
69a25ddd6c Merge pull request 'don't show non-message to user; fix API server setting; misc doc & task stuff' (#88) from adjust-note into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#88
2023-12-05 03:43:27 -05:00
a12d7fcc1b refactor task list 2023-12-04 20:02:09 -07:00
69c60e5426 change verbiage from "project" to "idea" 2023-12-04 19:55:57 -07:00
4806acc30e increase max characters for project description 2023-12-04 19:51:29 -07:00
1127d7079b remove outdated check, refactor tasks 2023-12-04 19:42:04 -07:00
0bbadfec6d add contact import by URL, add error notification, refine tasks 2023-12-04 19:21:03 -07:00
276d8b2f19 refine tasks & an error message 2023-12-04 17:27:36 -07:00
a7fbbbd4cd fix more paths where there may be no ID 2023-12-04 15:54:03 -07:00
a8d362c14d don't show note about registering if this user isn't registered 2023-12-04 13:36:51 -07:00
ce5933f645 remove visibility to contact operations where there is no activeDid 2023-12-04 13:29:16 -07:00
5cbf917ada don't show non-message to user; fix API server setting; misc doc & task stuff 2023-12-04 09:34:27 -07:00
7335412145 revert type complaint, which is opposite from previous suggestion, which 8-S 2023-12-04 09:31:24 -07:00
feea1a1d3b Merge pull request 'allow to customize the push-server for testing' (#80) from set-push-server into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#80
2023-12-04 10:59:22 -05:00
7f4d31a79c Merge branch 'master' into set-push-server 2023-12-04 08:55:39 -07:00
4041a7d08e more commentary, including for blank values for the user 2023-12-02 23:15:50 -07:00
Matthew Raymer
9846cf3e4c Some linting and further documenting 2023-12-02 22:08:04 -05:00
681d949098 update web push servers to the domains we're using 2023-12-02 15:35:44 -07:00
3bf8fd0c22 rename "push" to "webPush" for future-proofing 2023-12-02 15:28:32 -07:00
fa41fb3415 enhance documentation 2023-12-02 15:13:56 -07:00
6dbfc5f77d Merge pull request 'A cleaner attempt to merge' (#87) from service-worker-final into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#87
2023-12-01 22:36:35 -05:00
1b9ae96006 fix linting that caused failures 2023-12-01 11:06:50 -07:00
Matthew Raymer
4dd5664462 Fix exit from loops 2023-12-01 07:12:13 -05:00
Matthew Raymer
7d6a45061d A few missing configurations 2023-12-01 06:50:17 -05:00
Matthew Raymer
3b32c2b156 Some updates after a quick test run 2023-12-01 05:02:17 -05:00
Matthew Aaron Raymer
1ee6203f4c Small package update 2023-12-01 17:14:17 +08:00
Matthew Aaron Raymer
d93299c352 Update worker dependencies 2023-12-01 17:04:14 +08:00
Matthew Aaron Raymer
9aea7a576d Merging the workflow 2023-12-01 17:03:19 +08:00
714bb169fa Merge pull request 'fix keyword search to work for both local and everywhere searches' (#86) from searching into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#86
2023-12-01 01:28:30 -05:00
606d9ec734 fix the result count when the search filters change 2023-11-30 20:30:00 -07:00
7a3bd069b8 fix keyword search to work for both local and everywhere searches 2023-11-30 20:15:54 -07:00
b1ac9e71cb Merge pull request 'copy contact JWT URL; add project ID to URL' (#85) from more-small2 into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#85
2023-11-27 20:39:12 -05:00
c1176fa24d clear search results on restarting search 2023-11-26 20:08:30 -07:00
1cf6660e6c move both the contributions to & from this project to the last column (for when they both show) 2023-11-26 18:49:48 -07:00
6957678474 include project ID in the URLs to the project-details page 2023-11-26 18:42:28 -07:00
889b6d5737 update tasks 2023-11-26 18:22:35 -07:00
1be10b1511 add ability to copy the QR code (even though we don't have a way to read it in yet) 2023-11-26 18:20:15 -07:00
85405317ee update name of the app, and update version to 0.1.4 2023-11-23 11:15:48 -07:00
072497a553 Merge pull request 'add checkbox for given-to-user since sometimes it's to someone else' (#84) from to-me-checkbox into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#84
2023-11-21 22:03:22 -05:00
8a33ccfdcf add checkbox for given-to-user since sometimes it's to someone else 2023-11-20 20:41:26 -07:00
7311d36726 bump to v 0.1.4 2023-11-20 19:28:36 -07:00
7e819ea4de fix location checkbox so that we can uncheck it 2023-11-20 19:23:22 -07:00
5670f23bf3 Merge pull request 'record & list offers on a project' (#83) from many-misc into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#83
2023-11-20 00:37:02 -05:00
08d9ca3a25 Merge branch 'master' into many-misc 2023-11-20 00:36:49 -05:00
607666a2f9 add display of offers on project page 2023-11-19 19:44:11 -07:00
0a618cc4ff add button to record an offer on a project 2023-11-19 17:08:42 -07:00
e387794db3 Merge pull request 'much miscellany for small features and documentation & testing' (#82) from many-misc into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#82
2023-11-18 20:48:49 -05:00
ab1a725c1b fix & doc: don't show number of projects until loaded; refactor task list 2023-11-18 15:18:55 -07:00
46d76013e8 make a back button at the top and the tabs at the bottom of each page 2023-11-18 14:49:38 -07:00
faf8f4f6a9 feat: default to remote project search if no search area filter is selected 2023-11-18 14:38:55 -07:00
154fcd98a5 doc: refactor registration message & tests 2023-11-18 14:13:19 -07:00
c391385500 feat & doc: automatically set visibility & alert about registration, alert to help onboard (and refine docs & tasks) 2023-11-18 13:43:01 -07:00
b64f35869e fix: update openssl script according to new info 2023-11-18 06:25:30 -07:00
45fbf7ade5 Merge pull request 'Update docs, for openssl & tasks' (#81) from openssl into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#81
2023-11-17 23:49:29 -05:00
ee6a344daf doc: add a guess for the states of the notifications 2023-11-12 19:03:39 -07:00
65a5edf26b allow to customize the push-server for testing 2023-11-12 11:35:36 -07:00
70 changed files with 15938 additions and 6252 deletions

View File

@@ -9,7 +9,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.1.3] - 2023.11
## [0.1.8] - 2023.12.27
### Added
- DB logging for service-worker events
- Help page for notifications
- Test notification & web-push triggers inside app
- Check that the app is installed
### Fixed
- Project issuer display name
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
### Changed
- Icons
### Fixed
- Notification switch now shows message
- Prod/test server warning message at top of page
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
### Added
- Infinite scroll on home page
### Changed
- UI improvements
- Show web-push subscription info
- Icon
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
### Added
- Web push notifications (though not finalized)
- Credentials details page
- See more data without an ID
- Change units of a give
## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db
### Added
- Offer on a project
### Changed
- Automatically set as visible when importing a contact
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
### Added
- Contact name editing
### Changed

208
README.md
View File

@@ -1,4 +1,4 @@
# kickstart-for-time-pwa
# TimeSafari.app - Crowd-Funder for Time - PWA
## Project setup
@@ -13,44 +13,41 @@ npm install
npm run serve
```
### 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/",`
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Compiles and minifies for production
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
* 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/`
...to make sure the service worker scripts are in proper form. It's only important if you changed something in that directory.
* `cp sw_scripts/[ns]* dist/`
... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari`
* Revert src/constants/app.ts and/or package.json, edit package.json to increment version & add "-beta", `npm install`, and commit.
## Tests
### Web-push
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
### Test key contents
See [this page](openssl_signing_console.rst)
### Register new user on test server
New users require registration. This can be done with a claim payload like this
by an existing user:
```
const vcClaim = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { identifier: identity0.did },
object: SERVICE_ID,
participant: { identifier: newIdentity.did },
};
```
On the test server, User #0 has rights to register others, so you can start
playing one of two ways:
@@ -66,14 +63,39 @@ playing one of two ways:
### Create multiple identifiers
Go to /start and create or import a new one. Then switch identifiers on the bottom of the Your Identity page.
Under the "Your Identity" screen, click "Advanced", click "Switch Identity / No Identity", then "Add Another Identity...".
### Create keys with alternate tools
See [this page](openssl_signing_console.rst)
[This page](openssl_signing_console.rst) is a tool to create a JWT from a locally-generated keypair.
### Web-push
For your own web-push tests, change the push server URL in Advanced settings on the account page, and install Time Safari & push server on the same domain.
### Icons
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
### Manual walk-through
- Clear the browser cache for localhost for a new user.
- See that it's using the test API.
- On each page, verify the messaging.
- On the home page, see the feed without names, and see a message prompting to generate an ID.
- 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.)
- 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.
- On the home page, see the feed that shows User #0 with a name.
- Generate an ID.
- On the home page, check that it now prompts them to get registered.
- On the account page, check that they see messages on limits.
- Register the ID from User #0.
- As the new user on the home page, check that they can now record a gift.
- On the contacts page, check that they cannot register someone else yet.
- Walk through the functions on each page.
### Customize Vue configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Scenarios
@@ -81,17 +103,22 @@ See [Configuration Reference](https://cli.vuejs.org/config/).
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control`
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
### Clear data & restart
### Clear/Reset data & restart
* Clear 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".)
* 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.)
(If you find more, add them to the HelpNotificationsView.vue file.)
Clear cache for localhost, then go to http://localhost:8080/start
(because it'll generate a new one automatically if you start on the `/account` page).
@@ -102,110 +129,10 @@ Clear cache for localhost, then go to http://localhost:8080/start
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
* [Customize Vue configuration](https://cli.vuejs.org/config/).
// Import an existing ID
export const importAndStoreIdentifier = async (mnemonic: string, mnemonicPassword: string, toLowercase: boolean, previousIdentifiers: Array<IIdentifier>) => {
// just to get rid of variability that might cause an error
mnemonic = mnemonic.trim().toLowerCase()
/**
// an approach I pieced together
// requires: yarn add elliptic
// ... plus:
// const EC = require('elliptic').ec
// const secp256k1 = new EC('secp256k1')
//
const keyHex: string = bip39.mnemonicToEntropy(mnemonic)
// returns a KeyPair from the elliptic.ec library
const keyPair = secp256k1.keyFromPrivate(keyHex, 'hex')
// this code is from did-provider-eth createIdentifier
const privateHex = keyPair.getPrivate('hex')
const publicHex = keyPair.getPublic('hex')
const address = didJwt.toEthereumAddress(publicHex)
**/
/**
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
// ... which almost works but the didJwt.toEthereumAddress is wrong
// requires: yarn add bip32
// ... plus: import * as bip32 from 'bip32'
//
const seed: Buffer = await bip39.mnemonicToSeed(mnemonic)
const root = bip32.fromSeed(seed)
const node = root.derivePath(UPORT_ROOT_DERIVATION_PATH)
const privateHex = node.privateKey.toString("hex")
const publicHex = node.publicKey.toString("hex")
const address = didJwt.toEthereumAddress('0x' + publicHex)
**/
/**
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
// requires: yarn add @ethersproject/hdnode
// ... plus: import { HDNode } from '@ethersproject/hdnode'
**/
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic)
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH)
const privateHex = rootNode.privateKey.substring(2) // original starts with '0x'
const publicHex = rootNode.publicKey.substring(2) // original starts with '0x'
let address = rootNode.address
const prevIds = previousIdentifiers || [];
if (toLowercase) {
const foundEqual = R.find(
(id) => utility.rawAddressOfDid(id.did) === address,
prevIds
)
if (foundEqual) {
// They're trying to create a lowercase version of one that exists in normal case.
// (We really should notify the user.)
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a normal-case version of the DID since a regular version exists."}))
} else {
address = address.toLowerCase()
}
} else {
// They're not trying to convert to lowercase.
const foundLower = R.find((id) =>
utility.rawAddressOfDid(id.did) === address.toLowerCase(),
prevIds
)
if (foundLower) {
// They're trying to create a normal case version of one that exists in lowercase.
// (We really should notify the user.)
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a lowercase version of the DID since a lowercase version exists."}))
address = address.toLowerCase()
}
}
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... derived keys and address..."}))
const newId = newIdentifier(address, publicHex, privateHex, UPORT_ROOT_DERIVATION_PATH)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... created new ID..."}))
// awaiting because otherwise the UI may not see that a mnemonic was created
const savedId = await storeIdentifier(newId, mnemonic, mnemonicPassword)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... stored new ID..."}))
return savedId
}
// Create a totally new ID
export const createAndStoreIdentifier = async (mnemonicPassword) => {
// This doesn't give us the entropy/seed.
//const id = await agent.didManagerCreate()
const entropy = crypto.randomBytes(32)
const mnemonic = bip39.entropyToMnemonic(entropy)
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... generated mnemonic..."}))
return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, [])
}
```
## Kudos
### Kudos
Gifts make the world go 'round!
@@ -213,3 +140,4 @@ Gifts make the world go 'round!
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [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)

View File

@@ -7,16 +7,14 @@
# Usage: source ./openssl_signing_console.sh
#
# For a more complete explanation, see ./openssl_signing_console.rst
#
# It's crazy that raw execution only works about 20% of the time!
# See https://stackoverflow.com/questions/77505582/why-would-openssl-verify-succeed-every-time-with-source-but-fail-80-of-the
# Generate a key and extract the public part
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
openssl ec -in private.pem -pubout -out public.pem
# Use test data
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
@@ -24,13 +22,11 @@ payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '='
signing_input="$header_b64.$payload_b64"
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem | openssl base64 -e)
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature" | openssl base64 -d)
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
# Also tested this, to no avail.
#echo -n "$signature" > sig.out
#echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature sig.out

163
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "kickstart-for-time-pwa",
"version": "0.1.3",
"name": "TimeSafari_Test",
"version": "0.1.8",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@@ -33,6 +33,7 @@
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2",
"git-describe": "^4.1.1",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"localstorage-slim": "^2.5.0",
@@ -49,6 +50,7 @@
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2",
"three": "^0.156.1",
"util": "^0.12.5",
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.2",
@@ -72,13 +74,13 @@
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.15",
"eslint": "^8.48.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4",
"postcss": "^8.4.29",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tailwindcss": "^3.3.3",
"typescript": "~5.2.2"
}

View File

@@ -1,63 +1,28 @@
tasks:
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
- 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- 08 notifications :
- .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
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
- fix maskable icon
- Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery")
- 01 quick action - send action, maybe choose via canvas tool
- SEE: https://github.com/konvajs/vue-konva
- give users next public key hash
- 24 Move to Vite assignee:matthew
- .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
- .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView)
- .5 Add infinite scroll to gifts on the home page
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
- .1 when creating a plan, select location and then make sure you can deselect on Android
- .5 add link to further project / people when a project pays ahead
- .5 add project ID to the URL of the project-view, to make a project publicly-accessible
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- .1 Make give description text box into something that expands as they type
- .1 Make contact info specific to Time Safari - rather pointing at CommunityCred.org
- .5 If notifications are not enabled, add message to front page with link/button to enable
- Discuss whether the remaining tasks are worthwhile before MVP release.
- 04 allow user to download claims, mine + ones I can see about me from others
- 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path)
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages
- .5 customize favicon assignee-group:ui
- .2 Show a warning if both giver and recipient are the same (but still allow?)
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker
- .5 make a VC details page
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
- .5 include the hash of the latest commit on help page next to version
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
- contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 show error to user when adding a duplicate contact
- 01 parse input more robustly (with CSV lib and not commas)
- stats v1 :
- 01 show numeric stats
- 04 show different graphic for projects vs people on world
- 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- show VC details... somehow:
- 01 show my VCs - most interesting, or via search
- 01 allow download of each VC (& confirmations, to show that they actually own their data)
- 04 allow user to download VCs, mine + ones I can see about me from others
- add VC confirmation?
- Release Minimum Viable Product :
- .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).
@@ -66,12 +31,57 @@ tasks:
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
- Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- .5 show seed phrase in a QR code for transfer to another device
- 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) - on the second 'give' recorded on prod it showed me as the agent
- 01 send visibility signal as a VC and store it
- 04 remove 'rowid' references (that are sqlite-specific); may involve server
- 04 look at other examples for better UI friend.tech
- 01 make the prod build copy the sw_scripts
- .5 Add start date to project
- .3 check that Android shows "back" buttons on screens without bottom tray
- .1 Make give description text box into something that expands as they type?
- .5 customize favicon assignee-group:ui
- .2 Show a warning if both giver and recipient are the same (but still allow?)
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
- switch some checks for activeDid to check for isRegistered
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
- warn if they're using the web (android only?)
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
https://web.dev/articles/get-installed-related-apps
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
- contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 show error to user when adding a duplicate contact
- 01 parse input more robustly (with CSV lib and not commas)
- stats v1 :
- 01 show numeric stats
- 04 show different graphic for projects vs people (gnome?) on world
- 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- .5 show seed phrase in a QR code for transfer to another device
- .5 on DiscoverView, switch to a filter UI (eg. just from friend
- .5 don't show "Offer" on project screen if they aren't registered
- 24 Move to Vite
- 32 accept images for projects
- 32 accept images for contacts
- import project interactions from GitHub/GitLab and manage signing
- linking between projects or plans :
- show total time given to & from a project
@@ -94,23 +104,24 @@ tasks:
- automated tests, eg. cypress
- Notifications (wake on the phone, push notifications)
- pull instead of push, maybe via scheduled runs
- have a notification pop-up on Mac screen
- Connect with phone contacts
- Multiple identities
- Peer DID
- DIDComm
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
- Support KERI AIDs
- Support Peer DIDs
- Support messaging through DIDComm
- Write to or read from a different ledger (eg. private ACDC, EAS & attest.sh)
- Do we want split first name & last name?
- 40 notifications v+ :
- pull, w/ scheduled runs
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
- 16 From the home screen, make the quick action even easier.
- allow some gives even if they aren't registered - maybe someday as a gift to the world, but we really want this to be built via personal connections
log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,177 +0,0 @@
> kickstart-for-time-pwa@0.1.0 build
> vue-cli-service build
All browser targets in the browserslist configuration have supported ES module.
Therefore we don't build two separate bundles for differential loading.
WARNING Compiled with 5 warnings6:06:43 PM
[eslint]
/home/matthew/projects/kick-starter-for-time-pwa/src/components/World/components/objects/landmarks.js
98:11 warning Unexpected console statement no-console
133:7 warning Unexpected console statement no-console
144:5 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/router/index.ts
210:3 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/AccountViewView.vue
362:7 warning Unexpected console statement no-console
375:7 warning Unexpected console statement no-console
404:7 warning Unexpected console statement no-console
516:7 warning Unexpected console statement no-console
536:7 warning Unexpected console statement no-console
630:5 warning Unexpected console statement no-console
682:7 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactAmountsView.vue
206:9 warning Unexpected console statement no-console
233:9 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactGiftingView.vue
244:9 warning Unexpected console statement no-console
267:7 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactsView.vue
340:9 warning Unexpected console statement no-console
577:9 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/DiscoverView.vue
315:9 warning Unexpected console statement no-console
343:7 warning Unexpected console statement no-console
390:9 warning Unexpected console statement no-console
423:7 warning Unexpected console statement no-console
532:9 warning Unexpected console statement no-console
575:7 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/HomeView.vue
349:9 warning Unexpected console statement no-console
498:9 warning Unexpected console statement no-console
521:7 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/IdentitySwitcherView.vue
142:7 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportAccountView.vue
123:9 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportDerivedAccountView.vue
159:7 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/NewEditProjectView.vue
183:9 warning Unexpected console statement no-console
215:7 warning Unexpected console statement no-console
297:13 warning Unexpected console statement no-console
320:11 warning Unexpected console statement no-console
345:7 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectViewView.vue
387:9 warning Unexpected console statement no-console
421:7 warning Unexpected console statement no-console
457:7 warning Unexpected console statement no-console
552:9 warning Unexpected console statement no-console
554:11 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectsView.vue
131:9 warning Unexpected console statement no-console
144:7 warning Unexpected console statement no-console
221:9 warning Unexpected console statement no-console
237:7 warning Unexpected console statement no-console
/home/matthew/projects/kick-starter-for-time-pwa/src/views/SeedBackupView.vue
94:7 warning Unexpected console statement no-console
✖ 44 problems (0 errors, 44 warnings)
You may use special comments to disable some warnings.
Use // eslint-disable-next-line to ignore the next line.
Use /* eslint-disable */ to ignore all warnings in a file.
warning
/models/lupine_plant/textures/lambert2SG_baseColor.png is 3.75 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit.
warning
/models/lupine_plant/textures/lambert2SG_normal.png is 4.91 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit.
warning
asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
js/project.44f30c9f.js (318 KiB)
js/statistics.8a97010a.js (586 KiB)
js/chunk-vendors.a4845bfb.js (411 KiB)
js/705.f6a6ce2a.js (252 KiB)
img/textures/leafy-autumn-forest-floor.jpg (705 KiB)
models/lupine_plant/textures/lambert2SG_baseColor.png (3.58 MiB)
models/lupine_plant/textures/lambert2SG_normal.png (4.69 MiB)
warning
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
app (447 KiB)
js/chunk-vendors.a4845bfb.js
css/app.8f21529c.css
js/app.8833cebc.js
File Size Gzipped
dist/js/statistics.8a97010a.js 585.72 KiB 148.80 KiB
dist/js/chunk-vendors.a4845bfb.js 411.44 KiB 137.82 KiB
dist/js/project.44f30c9f.js 317.61 KiB 78.67 KiB
dist/js/705.f6a6ce2a.js 251.66 KiB 87.12 KiB
dist/js/891.33615e4f.js 147.32 KiB 42.09 KiB
dist/js/153.e2c8e249.js 146.26 KiB 42.21 KiB
dist/js/820.13565d16.js 66.10 KiB 18.33 KiB
dist/js/contact-qr.e170ec33.js 54.85 KiB 15.63 KiB
dist/js/772.7b4c53a7.js 30.29 KiB 7.21 KiB
dist/js/361.898a4525.js 27.40 KiB 8.19 KiB
dist/js/account.77d86130.js 17.51 KiB 5.93 KiB
dist/js/app.8833cebc.js 17.31 KiB 5.84 KiB
dist/js/contacts.3fc90ff8.js 16.94 KiB 5.52 KiB
dist/js/discover.24106939.js 15.30 KiB 5.22 KiB
dist/js/536.3bb13201.js 15.23 KiB 4.84 KiB
dist/workbox-5b385ed2.js 14.11 KiB 4.93 KiB
dist/js/home.218b99dd.js 13.89 KiB 4.97 KiB
dist/js/help.50d3117b.js 12.49 KiB 4.38 KiB
dist/js/projects.417a6cb7.js 8.71 KiB 3.00 KiB
dist/js/contact-amounts.a32b0ccd.js 8.44 KiB 3.25 KiB
dist/js/229.120e09bf.js 7.99 KiB 2.72 KiB
dist/js/identity-switcher.c7937333.js 7.44 KiB 2.52 KiB
dist/js/new-edit-project.0552181b.js 7.36 KiB 3.11 KiB
dist/js/300.dcaeb2a3.js 6.56 KiB 3.24 KiB
dist/js/seed-backup.76a0f7b3.js 3.99 KiB 1.97 KiB
dist/js/import-derive.c688d4b8.js 3.81 KiB 1.82 KiB
dist/js/import-account.c3fa35fd.js 3.54 KiB 1.66 KiB
dist/js/new-edit-account.bb763be2.js 3.39 KiB 1.51 KiB
dist/js/431.5a6d64e0.js 3.38 KiB 2.56 KiB
dist/service-worker.js 3.37 KiB 1.38 KiB
dist/js/scan-contact.46be989a.js 2.79 KiB 1.18 KiB
dist/js/start.091a7740.js 2.70 KiB 1.30 KiB
dist/js/new-identifier.bb379420.js 2.12 KiB 1.18 KiB
dist/js/93.b873dbbf.js 2.08 KiB 1.61 KiB
dist/js/new-edit-commitment.9248d367.j 1.96 KiB 1.05 KiB
s
dist/js/confirm-contact.02004d1d.js 1.89 KiB 1.04 KiB
dist/js/858.ae4c08ec.js 0.97 KiB 0.78 KiB
dist/css/app.8f21529c.css 18.41 KiB 4.39 KiB
dist/css/discover.73ee9bd3.css 14.77 KiB 6.25 KiB
dist/css/new-edit-project.73ee9bd3.css 14.77 KiB 6.25 KiB
dist/css/contacts.abb5e493.css 0.40 KiB 0.23 KiB
dist/css/contact-amounts.5b26ccd4.css 0.31 KiB 0.20 KiB
dist/css/home.828bc66e.css 0.25 KiB 0.19 KiB
dist/css/project.828bc66e.css 0.25 KiB 0.19 KiB
dist/css/statistics.828bc66e.css 0.25 KiB 0.19 KiB
Images and other types of assets omitted.
Build at: 2023-09-07T10:06:43.972Z - Hash: 2b39fcd4d0e78263 - Time: 32016ms
DONE Build complete. The dist directory is ready to be deployed.
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html

View File

@@ -169,20 +169,12 @@
>
Turn on Notifications
</button>
<div class="grid grid-cols-2 gap-2">
<button
@click="maybeLater(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Maybe Later
</button>
<button
@click="never(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
>
Never
</button>
</div>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Maybe Later
</button>
</div>
</div>
</div>
@@ -238,6 +230,10 @@
</p>
<button
@click="
close(notification.id);
turnOffNotifications();
"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
>
Turn Off Notifications
@@ -262,56 +258,170 @@
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import axios from "axios";
interface ServiceWorkerMessage {
type: string;
data: string;
}
interface ServiceWorkerResponse {
// Define the properties and their types
success: boolean;
message?: string;
}
// Example interface for error
interface ErrorResponse {
message: string;
// Other properties as needed
}
interface VapidResponse {
data: {
vapidKey: string;
};
}
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component
export default class App extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
b64 = "";
mounted() {
axios
.get("https://timesafari-pwa.anomalistlabs.com/web-push/vapid")
.then((response) => {
this.b64 = response.data.vapidKey;
console.log(this.b64);
})
.catch((error) => {
console.error("API error", error);
});
async mounted() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl = DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
await axios
.get(pushUrl + "/web-push/vapid")
.then((response: VapidResponse) => {
this.b64 = response.data?.vapidKey || "";
console.log("Got vapid key:", this.b64);
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.log("New service worker is now controlling the page");
});
});
if (!this.b64) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Could not set notifications.",
},
-1,
);
}
} catch (error) {
if (window.location.host.startsWith("localhost")) {
console.log("Ignoring the error getting VAPID for local development.");
} else {
console.error("Got an error initializing notifications:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Got an error setting notifications.",
},
-1,
);
}
}
}
private sendMessageToServiceWorker(
message: ServiceWorkerMessage,
): Promise<unknown> {
return new Promise((resolve, reject) => {
if (navigator.serviceWorker.controller) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event: MessageEvent) => {
if (event.data.error) {
reject(event.data.error as ErrorResponse);
} else {
resolve(event.data as ServiceWorkerResponse);
}
};
navigator.serviceWorker.controller.postMessage(message, [
messageChannel.port2,
]);
} else {
reject("Service worker controller not available");
}
});
}
private askPermission(): Promise<NotificationPermission> {
// Check if Notifications are supported
console.log("Requesting permission for notifications:", navigator);
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
return Promise.reject("Service worker not available.");
}
const secret = localStorage.getItem("secret");
if (!secret) {
return Promise.reject("No secret found.");
}
return this.sendSecretToServiceWorker(secret)
.then(() => this.checkNotificationSupport())
.then(() => this.requestNotificationPermission())
.catch((error) => Promise.reject(error));
}
private sendSecretToServiceWorker(secret: string): Promise<void> {
const message: ServiceWorkerMessage = {
type: "SEND_LOCAL_DATA",
data: secret,
};
return this.sendMessageToServiceWorker(message).then((response) => {
console.log("Response from service worker:", response);
});
}
private checkNotificationSupport(): Promise<void> {
if (!("Notification" in window)) {
alert("This browser does not support notifications.");
return Promise.reject("This browser does not support notifications.");
}
// Check existing permissions
if (Notification.permission === "granted") {
return Promise.resolve("granted");
return Promise.resolve();
}
return Promise.resolve();
}
// Request permission
return new Promise((resolve, reject) => {
const permissionResult = Notification.requestPermission((result) => {
resolve(result);
});
if (permissionResult) {
permissionResult.then(resolve, reject);
private requestNotificationPermission(): Promise<NotificationPermission> {
return Notification.requestPermission().then((permission) => {
if (permission !== "granted") {
alert(
"Allow this app permission to make notifications for personal reminders." +
" You can adjust them at any time in your settings.",
);
throw new Error("We weren't granted permission.");
}
}).then((permissionResult) => {
console.log("Permission result:", permissionResult);
if (permissionResult !== "granted") {
alert("We need notification permission to provide certain features.");
return Promise.reject("We weren't granted permission.");
}
return permissionResult;
return permission;
});
}
async turnOnNotifications() {
public async turnOnNotifications() {
return this.askPermission()
.then((permission) => {
console.log("Permission granted:", permission);
@@ -320,38 +430,51 @@ export default class App extends Vue {
this.subscribeToPush()
.then(() => {
console.log("Subscribed successfully.");
// Assuming the subscription object is available
return navigator.serviceWorker.ready;
})
.then((registration) => {
// Fetch the existing subscription object from the registration
return registration.pushManager.getSubscription();
})
.then((subscription) => {
if (subscription) {
console.log(subscription);
return this.sendSubscriptionToServer(subscription);
} else {
throw new Error("Subscription object is not available.");
}
})
.then(() => {
console.log("Subscription data sent to server.");
console.log(
"Subscription data sent to server and all finished successfully.",
);
this.$notify(
{
group: "alert",
type: "success",
title: "Notifications Turned On",
text: "Notifications are on. You should see one on your device; if not, see the 'Troubleshoot' page.",
},
-1,
);
})
.catch((error) => {
console.error(
"Subscription or server communication failed:",
error,
);
alert(
"Subscription or server communication failed. Try again in a while.",
);
});
})
.catch((error) => {
console.error("An error occurred:", error);
// Handle error appropriately here
console.error(
"An error occurred setting notification permissions:",
error,
);
alert("Some error occurred setting notification permissions.");
});
}
// Function to convert URL base64 to Uint8Array
private urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
@@ -366,41 +489,52 @@ export default class App extends Vue {
return outputArray;
}
// The subscribeToPush method
private subscribeToPush(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if ("serviceWorker" in navigator && "PushManager" in window) {
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
const options: PushSubscriptionOptions = {
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
};
console.log(options);
navigator.serviceWorker.ready
.then((registration) => {
return registration.pushManager.subscribe(options);
})
.then((subscription) => {
console.log("Push subscription successful:", subscription);
resolve();
})
.catch((error) => {
console.error("Push subscription failed:", error, options);
reject(error);
});
} else {
if (!("serviceWorker" in navigator && "PushManager" in window)) {
const errorMsg = "Push messaging is not supported";
console.warn(errorMsg);
reject(new Error(errorMsg));
return reject(new Error(errorMsg));
}
if (Notification.permission !== "granted") {
const errorMsg = "Notification permission not granted";
console.warn(errorMsg);
return reject(new Error(errorMsg));
}
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
const options: PushSubscriptionOptions = {
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
};
navigator.serviceWorker.ready
.then((registration) => {
return registration.pushManager.subscribe(options);
})
.then((subscription) => {
console.log("Push subscription successful:", subscription);
resolve();
})
.catch((error) => {
console.error("Push subscription failed:", error, options);
// Inform the user about the issue
alert(
"We encountered an issue setting up push notifications. " +
"If you wish to revoke notification permissions, please do so in your browser settings.",
);
reject(error);
});
});
}
private sendSubscriptionToServer(
subscription: PushSubscription,
): Promise<void> {
console.log(subscription);
console.log("About to send subscription...", subscription);
return fetch("/web-push/subscribe", {
method: "POST",
headers: {
@@ -415,12 +549,49 @@ export default class App extends Vue {
});
}
never(ID: string) {
alert(ID);
}
async turnOffNotifications() {
let subscription;
const pushProviderSuccess = await navigator.serviceWorker.ready
.then((registration) => {
return registration.pushManager.getSubscription();
})
.then((subscript) => {
subscription = subscript;
if (subscription) {
return subscription.unsubscribe();
} else {
console.log("Subscription object is not available.");
return false;
}
})
.catch((error) => {
console.log("Push provider server communication failed:", error);
return false;
});
maybeLater(ID: string) {
alert(ID);
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subscription),
})
.then((response) => {
return response.ok;
})
.catch((error) => {
console.log("Push server communication failed:", error);
return false;
});
alert(
"Notifications are off. Push provider unsubscribe " +
(pushProviderSuccess ? "succeeded" : "failed") +
(pushProviderSuccess === pushServerSuccess ? " and" : " but") +
" push server unsubscribe " +
(pushServerSuccess ? "succeeded" : "failed") +
".",
);
}
}
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -10,21 +10,24 @@
placeholder="What was received"
v-model="description"
/>
<div class="flex flex-row mb-6">
<div class="flex flex-row">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2"
>Hours</span
@click="changeUnitCode()"
>
{{ UNIT_SHORT[unitCode] }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()"
v-if="amountInput !== '0'"
>
<fa icon="chevron-left" />
</div>
<input
type="text"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="hours"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@@ -33,7 +36,13 @@
<fa icon="chevron-right" />
</div>
</div>
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
<div v-if="showGivenToUser" class="mt-2 text-right">
<input type="checkbox" class="mr-2" v-model="givenToUser" />
<label class="text-sm">Given to you</label>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
</p>
<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"
@click="confirm"
@@ -70,15 +79,36 @@ export default class GiftedDialog extends Vue {
@Prop message = "";
@Prop projectId = "";
@Prop showGivenToUser = false;
activeDid = "";
apiServer = "";
giver?: GiverInputInfo;
amountInput = "0";
giver?: GiverInputInfo; // undefined means no identified giver agent
description = "";
hours = "0";
givenToUser = false;
unitCode = "HUR";
visible = false;
/* eslint-disable prettier/prettier */
UNIT_SHORT: Record<string, string> = {
"BTC": "BTC",
"ETH": "ETH",
"HUR": "Hours",
"USD": "US $",
};
/* eslint-enable prettier/prettier */
/* eslint-disable prettier/prettier */
UNIT_LONG: Record<string, string> = {
"BTC": "BTC",
"ETH": "ETH",
"HUR": "hours",
"USD": "dollars",
};
/* eslint-enable prettier/prettier */
async created() {
try {
await db.open();
@@ -93,9 +123,7 @@ export default class GiftedDialog extends Vue {
group: "alert",
type: "danger",
title: "Error",
text:
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
text: err.message || "There was an error retrieving your settings.",
},
-1,
);
@@ -103,27 +131,47 @@ export default class GiftedDialog extends Vue {
}
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;
}
close() {
// close the dialog but don't change values (since it might be submitting info)
this.visible = false;
}
changeUnitCode() {
const units = Object.keys(this.UNIT_SHORT);
const index = units.indexOf(this.unitCode);
this.unitCode = units[(index + 1) % units.length];
}
increment() {
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
}
decrement() {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
this.amountInput = `${Math.max(
0,
(parseFloat(this.amountInput) || 1) - 1,
)}`;
}
cancel() {
this.close();
this.eraseValues();
}
eraseValues() {
this.description = "";
this.giver = undefined;
this.hours = "0";
this.givenToUser = this.showGivenToUser;
this.amountInput = "0";
}
async confirm() {
@@ -138,14 +186,13 @@ export default class GiftedDialog extends Vue {
1000,
);
// this is asynchronous, but we don't need to wait for it to complete
this.recordGive(
await this.recordGive(
this.giver?.did as string | undefined,
this.description,
parseFloat(this.hours),
parseFloat(this.amountInput),
this.unitCode,
).then(() => {
this.description = "";
this.giver = undefined;
this.hours = "0";
this.eraseValues();
});
}
@@ -169,12 +216,13 @@ export default class GiftedDialog extends Vue {
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
* @param amountInput may be 0
*/
public async recordGive(
giverDid?: string,
description?: string,
hours?: number,
amountInput?: number,
unitCode?: string,
) {
if (!this.activeDid) {
this.$notify(
@@ -189,13 +237,15 @@ export default class GiftedDialog extends Vue {
return;
}
if (!description && !hours) {
if (!description && !amountInput) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
text: `You must enter a description or some number of ${
this.UNIT_LONG[this.unitCode]
}.`,
},
-1,
);
@@ -209,9 +259,10 @@ export default class GiftedDialog extends Vue {
this.apiServer,
identity,
giverDid,
this.activeDid,
this.givenToUser ? this.activeDid : undefined,
description,
hours,
amountInput,
unitCode,
this.projectId,
);
@@ -238,7 +289,7 @@ export default class GiftedDialog extends Vue {
title: "Success",
text: "That gift was recorded.",
},
10000,
7000,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -0,0 +1,315 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Description, prerequisites, terms, etc."
v-model="description"
/>
<div class="flex flex-row mb-6">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
>
Hours
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()"
>
<fa icon="chevron-left" />
</div>
<input
type="text"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="hours"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<fa icon="chevron-right" />
</div>
</div>
<div class="flex flex-row mb-6">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
>
Expiration
</span>
<input
type="text"
class="w-full border border-slate-400 px-2 py-2 rounded-r"
:placeholder="'Date, eg. ' + new Date().toISOString().slice(0, 10)"
v-model="expirationDateInput"
/>
</div>
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
<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"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="cancel"
>
Cancel
</button>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { createAndSubmitOffer } from "@/libs/endorserServer";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component
export default class OfferDialog extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@Prop message = "";
@Prop projectId = "";
activeDid = "";
apiServer = "";
description = "";
expirationDateInput = "";
hours = "0";
visible = false;
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 || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.log("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your settings.",
},
-1,
);
}
}
open() {
this.visible = true;
}
close() {
this.visible = false;
}
increment() {
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
}
decrement() {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
}
cancel() {
this.close();
this.description = "";
this.hours = "0";
}
async confirm() {
this.close();
this.$notify(
{
group: "alert",
type: "toast",
text: "Recording the offer...",
title: "",
},
1000,
);
// this is asynchronous, but we don't need to wait for it to complete
this.recordOffer(
this.description,
parseFloat(this.hours),
this.expirationDateInput,
).then(() => {
this.description = "";
this.hours = "0";
});
}
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Offer records for DID ${activeDid} but no identity was found",
);
}
return identity;
}
/**
*
* @param description may be an empty string
* @param hours may be 0
*/
public async recordOffer(
description?: string,
hours?: number,
expirationDateInput?: string,
) {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record an offer.",
},
-1,
);
return;
}
if (!description && !hours) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
},
-1,
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitOffer(
this.axios,
this.apiServer,
identity,
description,
hours,
expirationDateInput,
this.projectId,
);
if (
result.type === "error" ||
this.isOfferCreationError(result.response)
) {
const errorMessage = this.getOfferCreationErrorMessage(result);
console.log("Error with offer creation result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error creating the offer.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That offer was recorded.",
},
10000,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error with offer recordation caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the offer.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: message,
},
-1,
);
}
}
// Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isOfferCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getOfferCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
);
}
}
</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

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

View File

@@ -4,20 +4,26 @@
* See also ../libs/veramo/setup.ts
*/
export enum AppString {
APP_NAME = "TimeSafari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
}
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host;
/**
* See notiwind package
* The possible values for "group" and "type" are in App.vue.
* From the notiwind package
*/
export interface NotificationIface {
group: string;
group: string; // "alert" | "modal"
type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string;
text: string;

View File

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

View File

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

10
src/db/tables/logs.ts Normal file
View File

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

View File

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

View File

@@ -12,6 +12,8 @@ export const SERVICE_ID = "endorser.ch";
export const CONTACT_URL_PREFIX = "https://endorser.ch";
// the suffix for the contact URL
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
// the prefix for handle IDs, the permanent ID for claims on Endorser
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
export interface AgreeVerifiableCredential {
"@context": string;
@@ -38,14 +40,24 @@ export interface ClaimResult {
error: { code: string; message: string };
}
export interface GenericClaim {
export interface GenericVerifiableCredential {
"@context": string;
"@type": string;
issuedAt: string;
// "any" because arbitrary objects can be subject of agreement
}
export interface GenericServerRecord extends GenericVerifiableCredential {
handleId?: string;
id?: string;
issuedAt?: string;
issuer?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>;
}
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "",
claim: {},
};
export interface GiveServerRecord {
agentDid: string;
@@ -59,17 +71,46 @@ export interface GiveServerRecord {
unit: string;
}
export interface OfferServerRecord {
amount: number;
amountGiven: number;
offeredByDid: string;
recipientDid: string;
requirementsMet: boolean;
unit: string;
validThrough: string;
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": string;
"@type": "GiveAction";
agent?: { identifier: string };
description?: string;
fulfills?: { "@type": string; identifier: string };
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string };
identifier?: string;
object?: { amountOfThisGood: number; unitCode: string };
recipient?: { identifier: string };
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": "Offer";
description?: string;
includesObject?: { amountOfThisGood: number; unitCode: string };
itemOffered?: {
description?: string;
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
};
offeredBy?: { identifier: string };
validThrough?: string;
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential {
"@context": "https://schema.org";
"@type": "PlanAction";
@@ -114,6 +155,104 @@ export function isHiddenDid(did: string) {
return did === HIDDEN_DID;
}
/**
* @return true for any nested string where func(input) === true
*
* Similar logic is found in endorser-mobile.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function testRecursivelyOnString(func: (arg0: any) => boolean, input: any) {
if (Object.prototype.toString.call(input) === "[object String]") {
return func(input);
} else if (input instanceof Object) {
if (!Array.isArray(input)) {
// it's an object
for (const key in input) {
if (testRecursivelyOnString(func, input[key])) {
return true;
}
}
} else {
// it's an array
for (const value of input) {
if (testRecursivelyOnString(func, value)) {
return true;
}
}
}
return false;
} else {
return false;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function containsHiddenDid(obj: any) {
return testRecursivelyOnString(isHiddenDid, obj);
}
export function stripEndorserPrefix(claimId: string) {
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
} else {
return claimId;
}
}
// similar logic is found in endorser-mobile
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function removeSchemaContext(obj: any) {
return obj["@context"] === SCHEMA_ORG_CONTEXT
? R.omit(["@context"], obj)
: obj;
}
// similar logic is found in endorser-mobile
export function addLastClaimOrHandleAsIdIfMissing(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
obj: any,
lastClaimId?: string,
handleId?: string,
) {
if (!obj.identifier && lastClaimId) {
const result = R.clone(obj);
result.lastClaimId = lastClaimId;
return result;
} else if (!obj.identifier && handleId) {
const result = R.clone(obj);
result.identifier = handleId;
return result;
} else {
return obj;
}
}
// return clone of object without any nested *VisibleToDids keys
// similar logic is found in endorser-mobile
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function removeVisibleToDids(input: any): any {
if (input instanceof Object) {
if (!Array.isArray(input)) {
// it's an object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: Record<string, any> = {};
for (const key in input) {
if (!key.endsWith("VisibleToDids")) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result[key] = removeVisibleToDids(R.clone(input[key]));
}
}
return result;
} else {
// it's an array
return R.map(removeVisibleToDids, input);
}
return false;
} else {
return input;
}
}
/**
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
@@ -134,8 +273,8 @@ export function didInfo(
return contact
? contact.name || "Contact With No Name"
: isHiddenDid(did)
? "Someone Not In Network"
: "Someone Not In Contacts";
? "Someone Not In Network"
: "Someone Not In Contacts";
}
export interface ResultWithType {
@@ -152,7 +291,7 @@ export interface ErrorResult {
error: InternalError;
}
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
/**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
@@ -171,21 +310,85 @@ export async function createAndSubmitGive(
toDid?: string,
description?: string,
hours?: number,
unitCode?: string,
fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitGiveResult> {
try {
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
recipient: toDid ? { identifier: toDid } : undefined,
agent: fromDid ? { identifier: fromDid } : undefined,
description: description || undefined,
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
fulfills: fulfillsProjectHandleId
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
: undefined,
};
): Promise<CreateAndSubmitClaimResult> {
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
recipient: toDid ? { identifier: toDid } : undefined,
agent: fromDid ? { identifier: fromDid } : undefined,
description: description || undefined,
object: hours
? { amountOfThisGood: hours, unitCode: unitCode || "HUR" }
: undefined,
fulfills: fulfillsProjectHandleId
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
: undefined,
};
return createAndSubmitClaim(
vcClaim as GenericServerRecord,
identity,
apiServer,
axios,
);
}
/**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
*
* @param identity
* @param description may be null; should have this or hours
* @param hours may be null; should have this or description
* @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
*/
export async function createAndSubmitOffer(
axios: Axios,
apiServer: string,
identity: IIdentifier,
description?: string,
hours?: number,
expirationDate?: string,
fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitClaimResult> {
const vcClaim: OfferVerifiableCredential = {
"@context": "https://schema.org",
"@type": "Offer",
offeredBy: { identifier: identity.did },
validThrough: expirationDate || undefined,
};
if (hours) {
vcClaim.includesObject = {
amountOfThisGood: hours,
unitCode: "HUR",
};
}
if (description) {
vcClaim.itemOffered = { description };
}
if (fulfillsProjectHandleId) {
vcClaim.itemOffered = vcClaim.itemOffered || {};
vcClaim.itemOffered.isPartOf = {
"@type": "PlanAction",
identifier: fulfillsProjectHandleId,
};
}
return createAndSubmitClaim(
vcClaim as GenericServerRecord,
identity,
apiServer,
axios,
);
}
export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential,
identity: IIdentifier,
apiServer: string,
axios: Axios,
): Promise<CreateAndSubmitClaimResult> {
try {
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
@@ -226,15 +429,11 @@ export async function createAndSubmitGive(
});
return { type: "success", response };
} catch (error: unknown) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error creating claim:", error);
const errorMessage: string =
error === null
? "Null error"
: error instanceof Error
? error.message
: typeof error === "object" && error !== null && "message" in error
? (error as { message: string }).message
: "Unknown error";
error.response?.data?.error?.message || error.message || "Unknown error";
return {
type: "error",
@@ -287,6 +486,10 @@ export interface ProjectData {
* URL referencing information about the project
**/
handleId: string;
/**
* The DID of the issuer
*/
issuerDid: string;
/**
* The Identier of the project
**/

5
src/libs/util.ts Normal file
View File

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

View File

@@ -13,7 +13,9 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import {
faArrowLeft,
faArrowRight,
faArrowUpRightFromSquare,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faChevronLeft,
@@ -27,6 +29,7 @@ import {
faCoins,
faComment,
faCopy,
faDollar,
faEllipsisVertical,
faEye,
faEyeSlash,
@@ -34,16 +37,19 @@ import {
faFloppyDisk,
faFolderOpen,
faGift,
faGlobe,
faHand,
faHouseChimney,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQuestion,
faQrcode,
faRotate,
faShareNodes,
@@ -60,7 +66,9 @@ import {
library.add(
faArrowLeft,
faArrowRight,
faArrowUpRightFromSquare,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faChevronLeft,
@@ -74,6 +82,7 @@ library.add(
faCoins,
faComment,
faCopy,
faDollar,
faEllipsisVertical,
faEye,
faEyeSlash,
@@ -81,17 +90,20 @@ library.add(
faFloppyDisk,
faFolderOpen,
faGift,
faGlobe,
faHand,
faHouseChimney,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQrcode,
faQuestion,
faRotate,
faShareNodes,
faSpinner,

View File

@@ -3,7 +3,7 @@
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
register("/additional-scripts.js", {
ready() {
console.log(
"App is being served from cache by a service worker.\n" +

View File

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

2184
src/util.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
@@ -32,8 +34,24 @@
</span>
</div>
<!-- ID notice -->
<div
v-if="!activeDid"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Note:</b> Before you can take any action, you need an ID.
</p>
<router-link
:to="{ name: 'start' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
>
Generate Identity
</router-link>
</div>
<!-- Registration notice -->
<!-- We won't show any loading indicator; we'll just pop the message in once we know 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
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
@@ -58,9 +76,9 @@
<span v-else>
<router-link
:to="{ name: 'new-edit-account' }"
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
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 name)
(Set Your Name)
</router-link>
</span>
@@ -87,54 +105,20 @@
</router-link>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<label
for="toggleNotifications"
class="flex items-center cursor-pointer"
@click="
this.$notify(
{
group: 'modal',
type: 'notification-permission',
},
-1,
)
"
<div
v-if="!notificationMaybeChanged"
class="flex items-center justify-between cursor-pointer"
@click="showNotificationChoice()"
>
<!-- label -->
<div>App Notifications</div>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" name="toggleNotifications" 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="toggleMuteNotifications"
class="flex items-center cursor-pointer mt-4"
@click="
this.$notify(
{
group: 'modal',
type: 'notification-mute',
},
-1,
)
"
>
<!-- label -->
<div>Mute Notifications</div>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
name="toggleMuteNotifications"
v-model="isSubscribed"
name="toggleNotificationsInput"
class="sr-only"
/>
<!-- line -->
@@ -144,14 +128,21 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
</div>
<div v-else>
Notification status may have changed. Revisit this page to see the
latest setting.
</div>
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
Test your notification setup.
</router-link>
</div>
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
<router-link
:to="{ name: 'seed-backup' }"
href=""
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"
>
Backup Identifier Seed
@@ -166,29 +157,42 @@
</a>
<a ref="downloadLink" />
<div v-if="activeDid" class="flex py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
<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 -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
<div v-if="loadingLimits" class="text-center mb-4">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
<div class="mb-4">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
<div v-if="!!limits?.nextWeekBeginDateTime">
<p class="mb-3 text-sm">
You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of
<b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims
counter resets at
<b class="whitespace-nowrap">{{
readableTime(limits.nextWeekBeginDateTime)
}}</b>
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
<p class="text-sm">
You have done
<b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of
<b>{{ limits.maxRegistrationsPerMonth }}</b> for this month.
<i
>(You can register nobody on your first day, and after that only one
a day in your first month.)</i
>
Your registration counter resets at
<b class="whitespace-nowrap">
{{ readableTime(limits.nextMonthBeginDateTime) }}
</b>
</p>
</div>
</div>
@@ -201,10 +205,14 @@
>
Advanced
</h3>
<div v-if="showAdvanced">
<p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedom!
</p>
<!-- Deep Identity Details -->
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
<h2 class="text-sm uppercase font-semibold mb-3">
Deep Identity Details
</h2>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
@@ -262,13 +270,11 @@
<label
for="toggleShowAmounts"
class="flex items-center cursor-pointer py-2"
class="flex items-center justify-between cursor-pointer my-4"
@click="handleChange"
>
<!-- label -->
<h2 class="text-slate-500 text-sm font-bold mb-2">
Show amounts given with contacts
</h2>
<h2>Show amounts given with contacts</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
@@ -287,26 +293,21 @@
</div>
</label>
<div class="flex py-2">
<button class="text-blue-500">
<!-- id used by puppeteer test script -->
<router-link
id="switch-identity-link"
:to="{ name: 'identity-switcher' }"
class="block text-center"
>
Switch Identity / No Identity
</router-link>
</button>
</div>
<router-link
:to="{ name: 'statistics' }"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
See Animated Global History of Giving
</router-link>
<div class="flex py-2">
<button class="text-blue-500">
<router-link :to="{ name: 'statistics' }" class="block text-center">
See Achievements & Statistics
</router-link>
</button>
</div>
<!-- id used by puppeteer test script -->
<router-link
id="switch-identity-link"
:to="{ name: 'identity-switcher' }"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Switch Identity
</router-link>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
@@ -323,35 +324,117 @@
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)"
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
>
Use Prod
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)"
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
>
Use Test
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)"
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
>
Use Local
</button>
</div>
<label
for="toggleProdWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleProdWarning"
>
<!-- label -->
<h2>Show warning if on prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<label
for="toggleTestWarningMessage"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleTestWarning"
>
<!-- label -->
<h2>Show warning if on non-prod server</h2>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<div class="flex py-4">
<h2 class="text-slate-500 text-sm font-bold mb-2">
Notification Push Server
</h2>
<input
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="webPushServerInput"
/>
<button
v-if="webPushServerInput != webPushServer"
class="px-4 rounded bg-red-500 border border-slate-400"
@click="onClickSavePushServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
>
Use Prod
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
>
Use Test 1
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
>
Use Test 2
</button>
</div>
<span class="px-4 text-sm" v-if="!webPushServerInput">
When that setting is blank, this app will use the default web push
server URL:
{{ AppConstants.DEFAULT_PUSH_SERVER }}
</span>
</div>
</section>
</template>
<script lang="ts">
import { AxiosError } from "axios";
import { AxiosError, AxiosRequestConfig } from "axios";
import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@@ -376,11 +459,11 @@ interface IAccount {
derivationPath: string;
}
@Component({ components: { QuickNav } })
@Component({ components: { QuickNav, TopMessage } })
export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString;
AppConstants = AppString;
activeDid = "";
apiServer = "";
@@ -388,9 +471,13 @@ export default class AccountViewView extends Vue {
derivationPath = "";
givenName = "";
isRegistered = false;
isSubscribed = false;
notificationMaybeChanged = false;
numAccounts = 0;
publicHex = "";
publicBase64 = "";
webPushServer = "";
webPushServerInput = "";
limits: RateLimits | null = null;
limitsMessage = "";
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
@@ -403,6 +490,65 @@ export default class AccountViewView extends Vue {
showAdvanced = false;
subscription: PushSubscription | null = null;
warnIfProdServer = false;
warnIfTestServer = false;
/**
* Async function executed when the component is created.
* Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
}
async mounted() {
try {
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
this.isSubscribed = !!this.subscription;
} catch (error) {
console.error("Mount error:", error);
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline;
this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
}
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
try {
// Open the accounts database
@@ -468,6 +614,16 @@ export default class AccountViewView extends Vue {
this.updateShowContactAmounts();
}
toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer;
this.updateWarnIfProdServer(this.warnIfProdServer);
}
toggleTestWarning() {
this.warnIfTestServer = !this.warnIfTestServer;
this.updateWarnIfTestServer(this.warnIfTestServer);
}
readableTime(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T"));
}
@@ -477,47 +633,6 @@ export default class AccountViewView extends Vue {
this.numAccounts = await accountsDB.accounts.count();
}
/**
* Async function executed when the component is created.
* Initializes the component's state with values from the database,
* handles identity-related tasks, and checks limitations.
*
* @throws Will display specific messages to the user based on different errors.
*/
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults
this.initializeState(settings);
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
} catch (err: unknown) {
this.handleError(err);
}
}
/**
* Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/
initializeState(settings: Settings | undefined) {
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline;
}
/**
* Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information.
@@ -542,6 +657,31 @@ export default class AccountViewView extends Vue {
}
}
async showNotificationChoice() {
if (!this.subscription) {
this.$notify(
{
group: "modal",
type: "notification-permission",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
} else {
this.$notify(
{
group: "modal",
type: "notification-off",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
}
this.notificationMaybeChanged = true;
}
/**
* Handles errors and updates the component's state accordingly.
* @param {Error} err - The error object.
@@ -580,12 +720,58 @@ export default class AccountViewView extends Vue {
group: "alert",
type: "danger",
title: "Error Updating Contact Setting",
text: "Clear your cache and start over (after data backup).",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to clear cache after contact setting update because:",
"Telling user to try again after contact setting update because:",
err,
);
}
}
public async updateWarnIfProdServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Prod Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after setting update because:",
err,
);
}
}
public async updateWarnIfTestServer(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Test Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after setting update because:",
err,
);
}
@@ -710,7 +896,7 @@ export default class AccountViewView extends Vue {
});
this.isRegistered = true;
} catch (err) {
console.log("Got an error updating settings:", err);
console.error("Got an error updating settings:", err);
this.$notify(
{
group: "alert",
@@ -739,7 +925,7 @@ export default class AccountViewView extends Vue {
private async fetchRateLimits(identity: IIdentifier) {
const url = `${this.apiServer}/api/report/rateLimits`;
const headers = await this.getHeaders(identity);
return await this.axios.get(url, { headers });
return await this.axios.get(url, { headers } as AxiosRequestConfig);
}
/**
@@ -752,10 +938,9 @@ export default class AccountViewView extends Vue {
const data = error.response?.data as ErrorResponse;
this.limitsMessage =
(data?.error?.message as string) || "Bad server response.";
console.log(
console.error(
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
this.limitsMessage,
//error,
);
} else if (
error instanceof Error &&
@@ -843,8 +1028,21 @@ export default class AccountViewView extends Vue {
this.apiServer = this.apiServerInput;
}
setApiServerInput(value: string) {
this.apiServerInput = value;
async onClickSavePushServer() {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
webPushServer: this.webPushServerInput,
});
this.webPushServer = this.webPushServerInput;
this.$notify(
{
group: "alert",
type: "warning",
title: "Reload",
text: "Now reload the app to get a new VAPID to use with this push server.",
},
-1,
);
}
}
</script>

476
src/views/ClaimView.vue Normal file
View File

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

View File

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

View File

@@ -66,7 +66,11 @@
</li>
</ul>
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
<GiftedDialog
ref="customDialog"
message="Received from"
showGivenToUser="true"
/>
</section>
</template>
@@ -141,6 +145,7 @@ export default class ContactGiftingView extends Vue {
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",
@@ -148,7 +153,7 @@ export default class ContactGiftingView extends Vue {
title: "Error",
text:
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
"There was an error retrieving your settings and/or contacts.",
},
-1,
);

View File

@@ -2,21 +2,55 @@
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info
</h1>
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!--
Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
-->
<QRCodeVue3
:value="this.qrValue"
:cornersSquareOptions="{ type: 'extra-rounded' }"
:dotsOptions="{ type: 'square' }"
class="flex justify-center"
/>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info
</h1>
<p v-if="!givenName" class="text-center mt-2">
<span class="text-red">Beware!</span>
You aren't sharing your name, so hurry and
<router-link
:to="{ name: 'new-edit-account' }"
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
>
go here to set it for them.
</router-link>
</p>
</div>
<div @click="onCopyToClipboard()" v-if="activeDid">
<!--
Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
-->
<QRCodeVue3
:value="this.qrValue"
:cornersSquareOptions="{ type: 'extra-rounded' }"
:dotsOptions="{ type: 'square' }"
class="flex justify-center"
/>
</div>
<div class="text-center" v-else>
You have no identitifiers yet, so
<router-link :to="{ name: 'start' }" class="text-blue-500">
create your identifier.
</router-link>
<br />
We recommend you do that first; otherwise, these contacts won't see your
activity.
</div>
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
@@ -27,6 +61,7 @@
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader";
import { useClipboard } from "@vueuse/core";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@@ -62,6 +97,7 @@ export default class ContactQRScanShow extends Vue {
activeDid = "";
apiServer = "";
givenName = "";
qrValue = "";
public async getIdentity(activeDid: string) {
@@ -75,7 +111,7 @@ export default class ContactQRScanShow extends Vue {
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
"Attempted to show contact info with no identity available.",
);
}
return identity;
@@ -86,21 +122,12 @@ export default class ContactQRScanShow extends Vue {
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.givenName = settings?.firstName || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (!account) {
this.$notify(
{
group: "alert",
type: "warning",
title: "",
text: "You have no identity yet.",
},
-1,
);
} else {
if (account) {
const identity = await this.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
@@ -137,7 +164,7 @@ export default class ContactQRScanShow extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) {
if (content[0]?.rawValue) {
console.log("onDetect", content[0].rawValue);
//console.log("onDetect", content[0].rawValue);
localStorage.setItem("contactEndorserUrl", content[0].rawValue);
this.$router.push({ name: "contacts" });
} else {
@@ -166,5 +193,22 @@ export default class ContactQRScanShow extends Vue {
-1,
);
}
onCopyToClipboard() {
useClipboard()
.copy(this.qrValue)
.then(() => {
console.log("Contact URL:", this.qrValue);
this.$notify(
{
group: "alert",
type: "toast",
title: "Copied",
text: "Contact URL was copied to clipboard.",
},
2000,
);
});
}
}
</script>

View File

@@ -19,15 +19,16 @@
</div>
<!-- New Contact -->
<div class="mb-4 flex">
<span class="self-center bg-slate-500 text-white px-1.5 py-1 rounded-md">
<router-link :to="{ name: 'contact-qr' }">
<fa icon="qrcode" class="fa-fw" />
</router-link>
</span>
<div class="mb-4 flex items-stretch">
<router-link
:to="{ name: 'contact-qr' }"
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw text-2xl" />
</router-link>
<input
type="text"
placeholder="DID, Name, Public Key"
placeholder="DID, Name, Public Key (base 16 or 64)"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
v-model="contactInput"
/>
@@ -67,8 +68,8 @@
showGiveTotals
? "Total"
: showGiveConfirmed
? "Confirmed"
: "Unconfirmed"
? "Confirmed"
: "Unconfirmed"
}}
</button>
<br />
@@ -110,52 +111,54 @@
</div>
<div id="ContactActions" class="flex gap-1.5 mt-2">
<button
v-if="contact.seesMe"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, false)"
title="They can see you"
>
<fa icon="eye" class="fa-fw" />
</button>
<button
v-else
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true)"
title="They cannot see you"
>
<fa icon="eye-slash" class="fa-fw" />
</button>
<button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)"
title="Check Visibility"
>
<fa icon="rotate" class="fa-fw" />
</button>
<button
@click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
>
<fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
<div v-if="activeDid">
<button
v-if="contact.seesMe"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, false, true)"
title="They can see you"
>
<fa icon="eye" class="fa-fw" />
</button>
<button
v-else
icon="person-circle-question"
class="fa-fw"
title="Registration Unknown"
/>
</button>
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true, true)"
title="They cannot see you"
>
<fa icon="eye-slash" class="fa-fw" />
</button>
<button
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)"
title="Check Visibility"
v-if="activeDid"
>
<fa icon="rotate" class="fa-fw" />
</button>
<button
@click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
v-if="activeDid"
>
<fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
v-else
icon="person-circle-question"
class="fa-fw"
title="Registration Unknown"
/>
</button>
</div>
<button
@click="deleteContact(contact)"
class="text-sm uppercase bg-red-600 text-white px-2 py-1.5 rounded-md"
class="text-sm uppercase bg-red-600 text-white ml-24 px-2 py-1.5 rounded-md"
title="Delete"
>
<fa icon="trash-can" class="fa-fw" />
@@ -168,7 +171,7 @@
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
@click="onClickAddGive(activeDid, contact.did)"
title="givenByMeDescriptions[contact.did]"
:title="givenByMeDescriptions[contact.did] || ''"
>
To:
{{
@@ -187,7 +190,7 @@
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
@click="onClickAddGive(contact.did, activeDid)"
title="givenToMeDescriptions[contact.did]"
:title="givenToMeDescriptions[contact.did] || ''"
>
From:
{{
@@ -263,6 +266,7 @@ import {
SimpleSigner,
} from "@/libs/crypto";
import {
CONTACT_URL_PREFIX,
GiveServerRecord,
GiveVerifiableCredential,
RegisterVerifiableCredential,
@@ -303,6 +307,7 @@ export default class ContactsView extends Vue {
givenToMeUnconfirmed: Record<string, number> = {};
hourDescriptionInput = "";
hourInput = "0";
isRegistered = false;
showGiveNumbers = false;
showGiveTotals = true;
showGiveConfirmed = true;
@@ -312,6 +317,7 @@ export default class ContactsView extends Vue {
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.isRegistered = !!settings?.isRegistered;
this.showGiveNumbers = !!settings?.showContactGivesInline;
if (this.showGiveNumbers) {
@@ -330,7 +336,7 @@ export default class ContactsView extends Vue {
}
}
public async getIdentity(activeDid: string) {
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
@@ -361,6 +367,10 @@ export default class ContactsView extends Vue {
}
async loadGives() {
if (!this.activeDid) {
return;
}
const handleResponse = (
resp: { status: number; data: { data: GiveServerRecord[] } },
descriptions: Record<string, string>,
@@ -395,11 +405,11 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Retrieval Error",
text:
"Got an error retrieving your " +
(useRecipient ? "given" : "received") +
" time from the server.",
" data from the server.",
},
-1,
);
@@ -450,12 +460,13 @@ export default class ContactsView extends Vue {
this.givenToMeConfirmed = givenToMeConfirmed;
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
} catch (error) {
console.log("Error loading gives", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
text: error as string,
title: "Load Error",
text: "Got an error loading your gives.",
},
-1,
);
@@ -475,6 +486,12 @@ export default class ContactsView extends Vue {
);
return;
}
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
await this.newContactFromScan(this.contactInput);
return;
}
let did = this.contactInput;
let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(",");
@@ -493,7 +510,7 @@ export default class ContactsView extends Vue {
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
}
const newContact = { did, name, publicKeyBase64 };
return this.addContact(newContact);
await this.addContact(newContact);
}
async newContactFromScan(url: string): Promise<void> {
@@ -531,6 +548,7 @@ export default class ContactsView extends Vue {
);
return;
}
newContact.seesMe = true; // since we will immediately set that on the server
return db.contacts
.add(newContact)
.then(() => {
@@ -539,15 +557,39 @@ export default class ContactsView extends Vue {
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
let addedMessage;
if (this.activeDid) {
this.setVisibility(newContact, true, false);
addedMessage =
newContact.name +
" was added, and your activity is visible to them.";
} else {
addedMessage = newContact.name + " was added.";
}
this.$notify(
{
group: "alert",
type: "success",
title: "Contact added",
text: newContact.name + " was added.",
title: "Contact Added",
text: addedMessage,
},
-1,
5000,
);
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) => {
console.error("Error when adding contact to storage:", err);
@@ -556,7 +598,7 @@ export default class ContactsView extends Vue {
group: "alert",
type: "danger",
title: "Contact Not Added",
text: "An error prevented importing.",
text: "An error prevented this import.",
},
-1,
);
@@ -566,11 +608,13 @@ export default class ContactsView extends Vue {
async deleteContact(contact: Contact) {
if (
confirm(
"Are you sure you want to delete " +
"You should first make sure that your activity is no longer visible to them." +
" Note that this only deletes them from your contacts on this device." +
" \n\nAre you sure you want to remove " +
this.nameForDid(this.contacts, contact.did) +
" with DID " +
contact.did +
" ?",
" from your contact list?",
)
) {
await db.open();
@@ -666,6 +710,7 @@ export default class ContactsView extends Vue {
);
}
} catch (error) {
console.error("Error when registering:", error);
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
@@ -682,7 +727,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Registration Error",
text: userMessage,
},
-1,
@@ -692,7 +737,11 @@ export default class ContactsView extends Vue {
}
}
async setVisibility(contact: Contact, visibility: boolean) {
async setVisibility(
contact: Contact,
visibility: boolean,
showSuccessAlert: boolean,
) {
const url =
this.apiServer +
"/api/report/" +
@@ -704,32 +753,48 @@ export default class ContactsView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.status === 200) {
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 || "Bad server response of " + resp.status;
resp.data.error?.message || "Got some error setting visibility.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Setting Visibility",
text: message,
},
-1,
);
}
} catch (err) {
console.error("Got some server error when setting visibility:", err);
console.error("Got some error when setting visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Setting Visibility",
text: "Check connectivity and try again.",
},
-1,
@@ -756,7 +821,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "info",
title: "Refreshed",
title: "Visibility Refreshed",
text:
this.nameForContact(contact, true) +
" can " +
@@ -772,19 +837,19 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Checking Visibility",
text: message,
},
-1,
);
}
} catch (err) {
console.log("Caught error from server request to check visibility:", err);
console.log("Caught error from request to check visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Checking Visibility",
text: "Check connectivity and try again.",
},
-1,
@@ -843,13 +908,13 @@ export default class ContactsView extends Vue {
},
-1,
);
} else if (!parseFloat(this.hourInput)) {
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Input Error",
text: "Giving 0 hours does nothing.",
text: "Giving no hours or descrption does nothing.",
},
-1,
);
@@ -900,6 +965,7 @@ export default class ContactsView extends Vue {
}
}
// similar function is in endorserServer.ts
private async createAndSubmitGive(
identity: IIdentifier,
fromDid: string,
@@ -969,6 +1035,7 @@ export default class ContactsView extends Vue {
}
}
} catch (error) {
console.log("Error in createAndSubmitGive: ", error);
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
@@ -985,7 +1052,7 @@ export default class ContactsView extends Vue {
{
group: "alert",
type: "danger",
title: "Server Error",
title: "Error Sending Give",
text: userMessage,
},
-1,

View File

@@ -1,5 +1,6 @@
<template>
<QuickNav selected="Discover"></QuickNav>
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
@@ -9,7 +10,7 @@
</h1>
<!-- Quick Search -->
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()">
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchSelected()">
<input
type="text"
v-model="searchTerms"
@@ -17,7 +18,7 @@
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/>
<button
@click="searchAll()"
@click="searchSelected()"
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
@@ -41,8 +42,10 @@
Nearby
<span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
>{{ localCount }}</span
v-if="isLocalActive"
>
{{ localCount > -1 ? localCount : "?" }}
</span>
</a>
</li>
<li>
@@ -59,8 +62,10 @@
Anywhere
<span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
>{{ remoteCount }}</span
v-if="isRemoteActive"
>
{{ remoteCount > -1 ? remoteCount : "?" }}
</span>
</a>
</li>
</ul>
@@ -86,7 +91,7 @@
</div>
<!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox">
<InfiniteScroll @reached-bottom="loadMoreData">
<ul>
<li
class="border-b border-slate-300"
@@ -132,6 +137,7 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
interface Notification {
group: string;
@@ -145,6 +151,7 @@ interface Notification {
QuickNav,
InfiniteScroll,
EntityIcon,
TopMessage,
},
})
export default class DiscoverView extends Vue {
@@ -156,13 +163,12 @@ export default class DiscoverView extends Vue {
apiServer = "";
searchTerms = "";
projects: ProjectData[] = [];
isChoosingSearchBox = false;
isLoading = false;
isLocalActive = true;
isRemoteActive = false;
localCount = 0;
remoteCount = 0;
localCount = -1;
remoteCount = -1;
searchBox: { name: string; bbox: BoundingBox } | null = null;
isLoading = false;
// make this function available to the Vue template
didInfo = didInfo;
@@ -180,7 +186,26 @@ export default class DiscoverView extends Vue {
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
this.searchLocal();
if (this.searchBox) {
await this.searchLocal();
} else {
this.isLocalActive = false;
this.isRemoteActive = true;
await this.searchAll();
}
}
public resetCounts() {
this.localCount = -1;
this.remoteCount = -1;
}
public async searchSelected() {
if (this.isLocalActive) {
await this.searchLocal();
} else {
await this.searchAll();
}
}
public async buildHeaders(): Promise<HeadersInit> {
@@ -208,6 +233,13 @@ export default class DiscoverView extends Vue {
}
public async searchAll(beforeId?: string) {
this.resetCounts();
if (!beforeId) {
// this was an initial search so clear any previous results
this.projects = [];
}
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
if (beforeId) {
@@ -245,8 +277,8 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data;
if (plans) {
for (const plan of plans) {
const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
const { name, description, handleId, issuerDid, rowid } = plan;
this.projects.push({ name, description, handleId, issuerDid, rowid });
}
this.remoteCount = this.projects.length;
} else {
@@ -270,13 +302,21 @@ export default class DiscoverView extends Vue {
}
public async searchLocal(beforeId?: string) {
this.resetCounts();
if (!this.searchBox) {
this.projects = [];
return;
}
if (!beforeId) {
// this was an initial search so clear any previous results
this.projects = [];
}
const claimContents =
"claimContents=" + encodeURIComponent(this.searchTerms);
let queryParams = [
claimContents,
"minLocLat=" + this.searchBox.bbox.minLat,
@@ -320,10 +360,14 @@ export default class DiscoverView extends Vue {
if (beforeId) {
const plans: ProjectData[] = results.data;
for (const plan of plans) {
const { name, description, handleId = plan.handleId, rowid } = plan;
if (beforeId !== plan["rowid"]) {
this.projects.push({ name, description, handleId, rowid });
}
const { name, description, handleId, issuerDid, rowid } = plan;
this.projects.push({
name,
description,
handleId,
issuerDid,
rowid,
});
}
} else {
this.projects = results.data;
@@ -371,7 +415,7 @@ export default class DiscoverView extends Vue {
onClickLoadProject(id: string) {
localStorage.setItem("projectId", id);
const route = {
name: "project",
path: "/project/" + encodeURIComponent(id),
};
this.$router.push(route);
}

View File

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

View File

@@ -1,11 +1,25 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Help
</h1>
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Help
</h1>
</div>
<div>
<p>
@@ -15,17 +29,18 @@
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<p>
We are building networks of people who want to grow a gifting society.
First of all, you can record ways you've seen people give, and that
leaves a permanent record -- one that came from you, and the recipient
can prove it was for them. This is personally gratifying, but it extends
to broader work: volunteers can get confirmation of activity and
selectively show off their contributions and network.
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
gifts you've seen, in a way that leaves a permanent record -- one that
came from you, and the recipient can prove it was for them. This is
personally gratifying, but it extends to broader work: volunteers get
confirmation of activity, and selectively show off their contributions
and network.
</p>
<p>
You can also record projects and plans and invite others to collaborate.
Soon you'll be able to see when others are interested and see how much
they're willing to contribute, even if there are conditions.
You can show giving and also offer help to ideas, based on others'
willingness to help out, too. You can record your own ideas and invite
others to collaborate.
</p>
<p>
This app uses the power of cryptography to build a reputation, recording
@@ -36,22 +51,39 @@
the control; this app gives you the control.
</p>
<h2 class="text-xl font-semibold">How do I take my first action?</h2>
<h2 class="text-xl font-semibold">How do I get started?</h2>
<p>
You need someone to register you -- usually the person who told you
about this app, on the Contacts
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
you can select any contact on the home page (or "anonymous") and record
your appreciation for... whatever. The main goal is to record what
people have given you, to grow gifting economies. Each claim is recorded
on a custom ledger. The day after being registered, you'll be able to
able to register others; later, you can create projects, too.
<fa icon="users" class="fa-fw" /> page. After they register you, you can
select any contact on the home page (or "anonymous") and record your
appreciation for... whatever. The main goal is to record what people
have given you, to grow giving economies. Each claim is recorded on a
custom ledger. The day after being registered, you'll be able to able to
register others; later, you can create projects, too.
</p>
<p>
Note that there are limits to how many others each person can register,
so you may have to wait.
</p>
<h2 class="text-xl font-semibold">How do I add someone else?</h2>
<p>
<button class="text-blue-500" @click="showOnboardInfo">
Click here to show an alert with the steps.
</button>
To start scanning, go
<router-link class="text-blue-500" to="/contact-qr">here.</router-link>
</p>
<p>
If they are not nearby to scan QR codes, tell them to copy their ID from
their Identity <fa icon="circle-user" class="fa-fw" /> page, which
typically starts with "did:ethr:...", and send it to you. Go to the
Contacts <fa icon="users" class="fa-fw" /> page and enter that into the
top form. To add a name, put a comma and then their name; to add their
public key, put another comma followed by the key.
</p>
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
<p>
There are two sets of data to backup: the identifier secrets and the
@@ -113,27 +145,20 @@
How do I restore my other (non-identifier-secret) data?
</h2>
<ul class="list-disc list-inside">
<li>Make sure you have your backup file (above), then contact us.</li>
<li>
Make sure you have your backup file (above), then contact us with
your interest. This is functionality that has to be written, and
your interest will help us prioritize it, but there are also manual
ways to restore your data.
</li>
</ul>
</div>
<h2 class="text-xl font-semibold">
How do I add someone to my contacts?
</h2>
<p>
Tell them to copy their ID, which typically starts with "did:ethr:...",
and send it to you. Go to the Contacts
<fa icon="circle-user" class="fa-fw" /> page and enter that into the top
form. You may add a name by adding a comma followed by their name; you
may also add their public key by adding another comma followed by the
key.
</p>
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
<p>
Before doing this, note that it is an advanced feature that affects
functionality (eg. the words "Alt ID" next to results, backup features)
so beware if you think that may cause confusion. You can
so beware. You can
<router-link to="start" class="text-blue-500">
create another identity here.
</router-link>
@@ -151,10 +176,34 @@
<fa icon="eye-slash" class="fa-fw" />.
</p>
<p>
Sometimes the reason you don't see something is because the search time
is limited. Go to the bottom and make sure to load all the data on a
list. If you still don't see it, try a search or view on a different
page.
Sometimes the reason you don't see something is because the search
results are limited. Go to the bottom and make sure to load all the data
on a list. If you still don't see it, try a search or view on a
different page.
</p>
<h2 class="text-xl font-semibold">
Where do I get help with notifications?
</h2>
<p>
<router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link
>
</p>
<h2 class="text-xl font-semibold">
How do I access even more functionality?
</h2>
<p>
There is an "Advanced" section at the bottom of the Account
<fa icon="circle-user" /> page.
</p>
<p>
There is a even more functionality in a mobile app (and more
documentation) at
<a href="https://endorser.ch" class="text-blue-500">
EndorserSearch.com
</a>
</p>
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
@@ -165,17 +214,24 @@
</a>
</p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<h2 class="text-xl font-semibold">Where can I read more?</h2>
<p>
{{ package.version }}
This is part of the
<a href="https://livesofgiving.org" class="text-blue-500">
Lives of Giving
</a>
initiative.
</p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<p>{{ package.version }} ({{ commitHash }})</p>
<h2 class="text-xl font-semibold">
For any other questions, including remove your data:
For any other questions, including removing your data:
</h2>
<p>
Contact us through
<a href="https://communitycred.org">CommunityCred.org</a>.
Contact us at
<a mailto="info@TimeSafari.app">info@TimeSafari.app</a>
</p>
</div>
</section>
@@ -186,8 +242,31 @@ import * as Package from "../../package.json";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class Help extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
package = Package;
commitHash = process.env.VUE_APP_GIT_HASH;
showOnboardInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "Onboard Someone",
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, preferably).
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,
);
}
}
</script>

View File

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

View File

@@ -76,7 +76,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
components: {},
})
export default class ImportAccountView extends Vue {
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
mnemonic = "";
address = "";

View File

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

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
[New/Edit] Plan
Edit Idea
</h1>
</div>
@@ -24,28 +24,34 @@
<input
type="text"
placeholder="Project Name"
placeholder="Idea Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="projectName"
v-model="fullClaim.name"
/>
<textarea
placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
rows="5"
v-model="description"
maxlength="500"
v-model="fullClaim.description"
maxlength="5000"
></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
{{ description.length }}/500 max. characters
{{ fullClaim.description?.length }}/5000 max. characters
</div>
<input
v-model="fullClaim.url"
placeholder="Website"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<div class="flex items-center mb-4">
<input
type="checkbox"
class="mr-2"
v-model="includeLocation"
@change="includeLocation = true"
@click="includeLocation = !includeLocation"
/>
<label for="includeLocation">Include Location</label>
</div>
@@ -72,7 +78,7 @@
name="OpenStreetMap"
/>
<l-marker
v-if="latitude || longitude"
v-if="latitude && longitude"
:lat-lng="[latitude, longitude]"
@click="maybeEraseLatLong()"
/>
@@ -136,13 +142,17 @@ export default class NewEditProjectView extends Vue {
activeDid = "";
apiServer = "";
description = "";
errorMessage = "";
fullClaim: PlanVerifiableCredential = {
"@context": "https://schema.org",
"@type": "PlanAction",
name: "",
description: "",
}; // this default is only to avoid errors before plan is loaded
includeLocation = false;
latitude = 0;
longitude = 0;
numAccounts = 0;
projectName = "";
zoom = 2;
async beforeCreate() {
@@ -214,9 +224,12 @@ export default class NewEditProjectView extends Vue {
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const claim = resp.data.claim;
this.projectName = claim.name;
this.description = claim.description;
this.fullClaim = resp.data.claim;
if (this.fullClaim?.location) {
this.includeLocation = true;
this.latitude = this.fullClaim.location.geo.latitude;
this.longitude = this.fullClaim.location.geo.longitude;
}
}
} catch (error) {
console.error("Got error retrieving that project", error);
@@ -225,13 +238,7 @@ export default class NewEditProjectView extends Vue {
private async SaveProject(identity: IIdentifier) {
// Make a claim
const vcClaim: PlanVerifiableCredential = {
"@context": "https://schema.org",
"@type": "PlanAction",
name: this.projectName,
description: this.description,
identifier: this.projectId || undefined,
};
const vcClaim: PlanVerifiableCredential = this.fullClaim;
if (this.projectId) {
vcClaim.identifier = this.projectId;
}
@@ -293,6 +300,20 @@ export default class NewEditProjectView extends Vue {
2000,
this,
);
} else {
console.log(
"Got unexpected 'data' inside response from server",
resp,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Saving Idea",
text: "Server did not save the idea. Try again.",
},
-1,
);
}
} catch (error) {
let userMessage = "There was an error saving the project.";
@@ -300,8 +321,8 @@ export default class NewEditProjectView extends Vue {
error?: { message?: string };
}>;
if (serverError) {
console.log("Got error from server", serverError);
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError);
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
this.$notify(
{

View File

@@ -1,11 +1,25 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Identity
</h1>
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Identity
</h1>
</div>
<div class="flex justify-center py-12">
<span />
@@ -74,7 +88,7 @@ export default class NewIdentifierView extends Vue {
this.loading = false;
setTimeout(() => {
this.$router.push({ name: "account" });
this.$router.push({ name: "home" });
}, 1000);
}
}

View File

@@ -1,5 +1,7 @@
<template>
<QuickNav selected="Projects"></QuickNav>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
@@ -12,7 +14,7 @@
>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
View Plan
Idea
</h1>
</div>
@@ -35,7 +37,7 @@
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{ issuer }}
</div>
<div>
<div v-if="timeSince">
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
{{ timeSince }}
</div>
@@ -45,8 +47,14 @@
:href="getOpenStreetMapUrl()"
target="_blank"
class="underline"
>
Map View
>Map View
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
<div v-if="url">
<fa icon="globe" class="fa-fw text-slate-400"></fa>
<a :href="addScheme(url)" target="_blank" class="underline"
>{{ domainForWebsite(this.url) }}
</a>
</div>
</div>
@@ -56,8 +64,11 @@
<div class="text-sm text-slate-500">
<div v-if="!expanded">
{{ truncatedDesc }}
<a v-if="description.length >= truncateLength" @click="expandText"
>Read More</a
<a
v-if="description.length >= truncateLength"
@click="expandText"
class="uppercase text-xs font-semibold text-slate-700"
>... Read More</a
>
</div>
<div v-else>
@@ -65,7 +76,7 @@
<a
@click="collapseText"
class="uppercase text-xs font-semibold text-slate-700"
>Read Less</a
>- Read Less</a
>
</div>
</div>
@@ -80,20 +91,30 @@
</button>
</div>
<div>
<div v-if="activeDid" class="text-center">
<div v-if="activeDid" class="mb-4">
<div class="text-center">
<button
@click="openDialog({ name: 'you', did: activeDid })"
@click="openOfferDialog({ name: 'you', did: activeDid })"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
>
I offer&hellip;
</button>
</div>
</div>
<div v-if="activeDid">
<div class="text-center">
<button
@click="openGiftDialog({ name: 'you', did: activeDid })"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
>
I gave&hellip;
</button>
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
<p class="mt-2 mb-4 text-center">Or, record a contribution from:</p>
</div>
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()">
<li @click="openGiftDialog()">
<EntityIcon
:entityId="null"
:iconSize="64"
@@ -108,7 +129,7 @@
<li
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
@click="openGiftDialog(contact)"
>
<EntityIcon
:entityId="contact.did"
@@ -134,12 +155,48 @@
</div>
<!-- Gifts to & from this -->
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">
Given To This Project
Offered To This Idea
</h3>
<div v-if="offersToThis.length === 0">
(None yet. Record one above.)
</div>
<ul v-else class="text-sm border-t border-slate-300">
<li
v-for="offer in offersToThis"
:key="offer.id"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
<span>
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{ didInfo(offer.agentDid, activeDid, allMyDids, allContacts) }}
</span>
<a @click="onClickLoadClaim(offer.jwtId)">
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
</a>
<span v-if="offer.amount">
<fa
:icon="iconForUnitCode(offer.unit)"
class="fa-fw text-slate-400"
/>{{ offer.amount }}
</span>
</div>
<div v-if="offer.objectDescription" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400"></fa>
{{ offer.objectDescription }}
</div>
</li>
</ul>
</div>
<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>
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
<ul v-else class="text-sm border-t border-slate-300">
@@ -153,9 +210,14 @@
><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
</span>
<span v-if="give.amount"
><fa icon="coins" class="fa-fw text-slate-400"></fa>
{{ give.amount }}
<a @click="onClickLoadClaim(give.jwtId)">
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
</a>
<span v-if="give.amount">
<fa
:icon="iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div v-if="give.description" class="text-slate-500">
@@ -166,44 +228,52 @@
</ul>
</div>
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">
Contributions By This Project
</h3>
<button
@click="onClickLoadProject(fulfilledByThis.handleId)"
class="text-blue-500"
<div class="grid items-start grid-cols-1 gap-4">
<div
v-if="fulfillersToThis.length > 0"
class="bg-slate-100 px-4 py-3 rounded-md"
>
{{ fulfilledByThis.name }}
</button>
</div>
<h3 class="text-sm uppercase font-semibold mb-3">
Contributions To This Idea
</h3>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center">
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
<button
@click="onClickLoadProject(plan.handleId)"
class="text-blue-500"
>
{{ plan.name }}
</button>
</div>
</div>
</div>
<div
v-if="fulfillersToThis.length > 0"
class="bg-slate-100 px-4 py-3 rounded-md"
>
<h3 class="text-sm uppercase font-semibold mb-3">
Contributions To This Project
</h3>
<ul>
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">
Contributions By This Idea
</h3>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center">
<button
@click="onClickLoadProject(plan.handleId)"
@click="onClickLoadProject(fulfilledByThis.handleId)"
class="text-blue-500"
>
{{ plan.name }}
{{ fulfilledByThis.name }}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<GiftedDialog
ref="customDialog"
ref="customGiveDialog"
message="Received from"
:projectId="this.projectId"
>
</GiftedDialog>
<OfferDialog ref="customOfferDialog" :projectId="this.projectId">
</OfferDialog>
</section>
</template>
@@ -214,14 +284,18 @@ import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue";
import TopMessage from "@/components/TopMessage.vue";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { isGlobalUri } from "@/libs/util";
import {
didInfo,
GiverInputInfo,
GiveServerRecord,
OfferServerRecord,
PlanServerRecord,
} from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
@@ -236,7 +310,7 @@ interface Notification {
}
@Component({
components: { GiftedDialog, QuickNav, EntityIcon },
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
})
export default class ProjectViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@@ -250,14 +324,16 @@ export default class ProjectViewView extends Vue {
fulfilledByThis: PlanServerRecord | null = null;
fulfillersToThis: Array<PlanServerRecord> = [];
givesToThis: Array<GiveServerRecord> = [];
issuer = "";
latitude = 0;
longitude = 0;
name = "";
issuer = "";
offersToThis: Array<OfferServerRecord> = [];
projectId = localStorage.getItem("projectId") || ""; // handle ID
timeSince = "";
truncatedDesc = "";
truncateLength = 40;
url = "";
async created() {
await db.open();
@@ -287,12 +363,6 @@ export default class ProjectViewView extends Vue {
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
@@ -359,19 +429,22 @@ export default class ProjectViewView extends Vue {
this.truncatedDesc = this.description.slice(0, this.truncateLength);
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
} else if (resp.status === 404) {
// actually, axios throws an error so we never get here
this.url = resp.data.claim?.url || "";
} else {
// actually, axios throws an error on 404 so we probably never get here
console.log("Error getting project:", resp);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "That project does not exist.",
text: "There was a problem getting that project. See logs for more info.",
},
-1,
);
}
} catch (error: unknown) {
console.error("Error retrieving project:", error);
const serverError = error as AxiosError;
if (serverError.response?.status === 404) {
this.$notify(
@@ -393,7 +466,6 @@ export default class ProjectViewView extends Vue {
},
-1,
);
console.error("Error retrieving project:", serverError.message);
}
}
@@ -433,6 +505,42 @@ export default class ProjectViewView extends Vue {
);
}
const offersToUrl =
this.apiServer +
"/api/v2/report/offersToPlans?planIds=" +
encodeURIComponent(JSON.stringify([projectId]));
try {
const resp = await this.axios.get(offersToUrl, { headers });
if (resp.status === 200 && resp.data.data) {
this.offersToThis = resp.data.data;
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to retrieve offers to this project.",
},
-1,
);
}
} catch (error: unknown) {
const serverError = error as AxiosError;
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving offers to this project.",
},
-1,
);
console.error(
"Error retrieving offers to this project:",
serverError.message,
);
}
const fulfilledByUrl =
this.apiServer +
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
@@ -533,8 +641,66 @@ export default class ProjectViewView extends Vue {
);
}
openDialog(contact: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(contact);
openGiftDialog(contact: GiverInputInfo) {
(this.$refs.customGiveDialog as GiftedDialog).open(contact);
}
openOfferDialog() {
(this.$refs.customOfferDialog as OfferDialog).open();
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
this.$router.push(route);
}
UNIT_CODES: Record<string, Record<string, string>> = {
BTC: {
name: "Bitcoin",
faIcon: "bitcoin-sign",
},
HUR: {
name: "hours",
faIcon: "clock",
},
USD: {
name: "US Dollars",
faIcon: "dollar",
},
};
iconForUnitCode(unitCode: string) {
return this.UNIT_CODES[unitCode]?.faIcon || "question";
}
// return an HTTPS URL if it's not a global URL
addScheme(url: string) {
if (!isGlobalUri(url)) {
return "https://" + url;
}
return url;
}
// return just the domain for display, if possible
domainForWebsite(url: string) {
try {
const hostname = new URL(url).hostname;
if (!hostname) {
// happens for non-http URLs
return url;
} else if (url.endsWith(hostname)) {
// it's just the domain
return hostname;
} else {
// there's more, but don't bother displaying the whole thing
return hostname + "...";
}
} catch (error: unknown) {
// must not be a valid URL
return url;
}
}
}
</script>

View File

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

View File

@@ -1,4 +1,6 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
@@ -105,6 +107,7 @@ import {
import { db } from "@/db/index";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue";
const DEFAULT_LAT_LONG_DIFF = 0.01;
const WORLD_ZOOM = 2;
@@ -119,6 +122,7 @@ interface Notification {
@Component({
components: {
QuickNav,
LRectangle,
LMap,
LMarker,
@@ -214,6 +218,7 @@ export default class DiscoverView extends Vue {
},
-1,
);
this.$router.back();
} catch (err) {
this.$notify(
{

View File

@@ -3,10 +3,23 @@
id="Content"
class="p-6 pb-24 min-h-screen flex flex-col justify-center"
>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Start Here
</h1>
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Start Here
</h1>
</div>
<!-- id used by puppeteer test script -->
<div id="start-question" class="mt-8">

View File

@@ -1,11 +1,25 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Achievements & Statistics
</h1>
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Achievements & Statistics
</h1>
</div>
<div>
Here is a view of the activity you can see.
@@ -32,7 +46,7 @@
{{ worldProperties.animationDurationSeconds }} seconds
</div>
</div>
<button class="float-right" @click="captureGraphics()">Screenshot</button>
<button class="float-right text-blue-600" @click="captureGraphics()">Screenshot</button>
<div id="scene-container" class="h-screen"></div>
</section>
</template>

View File

@@ -1,11 +1,25 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Test
</h1>
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Test
</h1>
</div>
<div class="mb-8">
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>

View File

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

1051
sw_scripts/nacl.js Normal file

File diff suppressed because it is too large Load Diff

5248
sw_scripts/noble-curves.js Normal file

File diff suppressed because it is too large Load Diff

3068
sw_scripts/noble-hashes.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,8 @@
const { defineConfig } = require("@vue/cli-service");
const { gitDescribeSync } = require("git-describe");
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
@@ -11,8 +15,9 @@ module.exports = defineConfig({
iconPaths: {
faviconSVG: "img/icons/safari-pinned-tab.svg",
},
workboxPluginMode: "InjectManifest",
workboxOptions: {
importScripts: ["additional-scripts.js"],
swSrc: "./sw_scripts/additional-scripts.js",
},
},
});

View File

@@ -390,3 +390,32 @@ While notifications are turned on, the user can tap on the App Notifications tog
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
- "Leave it On" to make no changes and dismiss the dialog.
# NOTIFICATION STATES
* Unpermissioned. Push server cannot send notifications to the user because it does not have permission.
This may be the same as when the user gave permission in the past but has since revoked it at the OS or browser
level, outside the app. (User can change to Permissioned when the user gives permission.)
* Permissioned. (User can change to Unpermissioned via the OS or browser settings.)
* Active. (User can change to Muted when the user mutes notifications.)
* Muted. (User can change to Active when the user toggles it.)
(Turning mute off automatically after some amount of time is not planned in version 1.)
# TROUBLESHOOTING
## Desktop
#### Firefox
Go to `about:debugging` and click on `Inspect` for the service worker.
#### Chrome
Go to `chrome://inspect/#service-workers` and click on `Inspect` for the service worker.
## Mobile
#### Android
#### iOS