Compare commits

...

709 Commits

Author SHA1 Message Date
Jose Olarte III
a810d531b7 In-progress: PWA install test 2024-08-22 21:24:05 +08:00
Jose Olarte III
91445cc240 Merge branch 'master' into test-playwright 2024-08-22 18:05:35 +08:00
85b9aa8e2b fix linting 2024-08-21 19:16:00 -06:00
7309ba1436 add tests for importing multiple records, fix other confirmation tests 2024-08-21 18:43:28 -06:00
Jose Olarte III
07efab3782 Playwright: Record 10 gives 2024-08-21 19:47:48 +08:00
Jose Olarte III
375cda1082 Merge branch 'master' into test-playwright 2024-08-21 15:22:22 +08:00
67b0122d5a fix tests 2024-08-20 20:05:04 -06:00
6aef08d7e8 copy a list of contacts and then import 2024-08-20 19:39:29 -06:00
a5248af4a3 move contact actions into the details page (prepping for checkboxes) 2024-08-19 20:18:06 -06:00
d9f45d52f9 bump verison and add "-beta" 2024-08-18 17:51:31 -06:00
dc80b686ce bump to version 0.3.20 2024-08-18 17:04:14 -06:00
892cf4c595 update bad verbiage on offer page, fix offer test 2024-08-18 17:02:15 -06:00
e2b641736d bump verison and add "-beta" 2024-08-18 14:58:01 -06:00
bb1fc7519f bump to version 0.3.19 2024-08-18 14:14:53 -06:00
014d4081e6 fix error editing an offer, tweak tests to fix red in IntelliJ 2024-08-18 14:13:42 -06:00
877678b745 bump to version 0.3.18 2024-08-18 13:52:55 -06:00
a3da157ae3 Merge pull request 'offer editing' (#123) from offer-edit into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#123
2024-08-18 15:48:53 -04:00
4f97010f99 fix tests, add test for offer update 2024-08-18 13:48:07 -06:00
f38edff942 allow editing of an offer 2024-08-17 19:59:02 -06:00
73c82aefe2 start with offer-edit 2024-08-16 15:58:54 -06:00
7df6668dc6 put BTC before BX in unit rotation 2024-08-15 19:41:18 -06:00
60e2d549cc fix destination page after photo is shared 2024-08-14 08:56:57 -06:00
e5155a3da1 add recipient description to offers in user's list 2024-08-12 20:38:54 -06:00
b922675491 misc commentary 2024-08-12 18:51:41 -06:00
53e77e46dd fix list of offers (and some other lists), and add tests for offers 2024-08-12 09:25:01 -06:00
8c652ab29b change back the check for adding a service worker because tests would get constant errors 2024-08-12 09:23:25 -06:00
06d9052386 bump version and add "-beta" 2024-08-12 09:19:15 -06:00
0e2c4ed08b bump to version 0.3.17 2024-08-12 09:17:32 -06:00
Jose Olarte III
713faebf51 DONE: create 10 projects 2024-08-12 19:54:25 +08:00
Jose Olarte III
93a230298d Merge branch 'master' into test-playwright 2024-08-12 16:40:05 +08:00
86063b27e8 remove "export" that's not available in raw JS 2024-08-11 19:01:34 -06:00
57fe2cbe13 fix image shared with web share 2024-08-11 08:17:27 -06:00
6b4b3642f9 record some info on my attempt to test a service worker 2024-08-10 20:09:49 -06:00
844a462482 bump version and add "-beta" 2024-08-10 16:11:28 -06:00
d52f0a106a bump to version 0.3.16 2024-08-10 16:08:34 -06:00
a001f2fde3 fix linting, and give instructions for current test suite 2024-08-10 13:37:31 -06:00
5ad933f1c6 fix a test, add potential-failing comment 2024-08-10 08:11:31 -06:00
Jose Olarte III
799c8d66c0 IN-PROGRESS: create 10 projects 2024-08-09 23:04:25 +08:00
93caec3719 add image on entries in a project 2024-08-09 07:55:31 -06:00
e30e43d762 show image on the view-claim screen 2024-08-09 07:27:29 -06:00
c69c3a7126 refactor confirmation section to show together and more cleanly 2024-08-09 07:20:01 -06:00
bdb544a624 fix error sharing image and failing to upload, fix upload in webkit/safari, and test it 2024-08-08 08:51:25 -06:00
Jose Olarte III
c8bdaa10eb Playwright: added ID to spinbutton 2024-08-08 15:47:19 +08:00
f17f830453 tweak instructions for minimal test data 2024-08-07 09:25:30 -06:00
Jose Olarte III
761c49de45 (Switch back to test server) 2024-08-07 22:06:36 +08:00
Jose Olarte III
6474ae1f4b Playwright: expended contact test 2024-08-07 21:51:24 +08:00
Jose Olarte III
5fef073839 Playwright: test against created records 2024-08-06 20:12:34 +08:00
Jose Olarte III
a2164d8791 Playwright: added import 2024-08-06 16:20:52 +08:00
Jose Olarte III
128b18ab56 Playwright: removed redundant tests 2024-08-05 19:59:40 +08:00
Jose Olarte III
3da4b2bf9e Playwright: combined no-ID tests 2024-08-05 19:59:25 +08:00
Jose Olarte III
5da836c47c Playwright: implemented importUser 2024-08-05 19:58:40 +08:00
Jose Olarte III
43965e2ea7 Playwright: importUser function 2024-08-05 19:56:42 +08:00
2e6bd3bd9f bump version and add "-beta" 2024-08-04 20:26:56 -06:00
d3e5ac5c37 bump to version 0.3.15, fix a README instruction 2024-08-04 20:01:26 -06:00
db1291836e remove unused ethr-did-resolver (since it has vulerabilities and we're not using it and we can use the local one) 2024-08-04 19:59:02 -06:00
e0c50dcf62 add 'isRegistered' check to guard against many buttons 2024-08-04 19:56:10 -06:00
6bac80a280 move pointers to other projects up in the project view 2024-08-04 19:09:11 -06:00
61fffbb13e add a test for empty ID, fix some linting 2024-08-04 07:30:35 -06:00
0abe3aebee remove unused code 2024-08-02 20:38:21 -06:00
1ca61d72c9 comment out a breaking test on local data & enhance those instructions 2024-08-02 18:58:39 -06:00
Jose Olarte III
0f7d13ebf9 Playwright: check ID generation 2024-07-31 19:44:44 +08:00
Jose Olarte III
8008504828 Playwright: additional checks to add contact 2024-07-30 19:13:18 +08:00
Jose Olarte III
2aead1b4b1 ID-specific locators 2024-07-30 18:50:53 +08:00
Jose Olarte III
37d4e36561 Mirrored browser selection 2024-07-30 16:36:11 +08:00
Jose Olarte III
a410836539 Added IDs for Playwright targeting 2024-07-30 16:35:55 +08:00
5334c5970b make instructions for an Endorser server started from scratch 2024-07-29 19:19:07 -06:00
Jose Olarte III
421101a2c9 Playwright: check usage limits (no-ID and with-ID) 2024-07-29 19:09:56 +08:00
Jose Olarte III
ef2430319d Playwright: confirm contact appears on home feed 2024-07-29 19:09:35 +08:00
Jose Olarte III
36faf15a62 Corrected some test labels 2024-07-29 17:01:50 +08:00
710e00fdc2 add visibility flag set, refactor to see results, and add copy icons for contact info 2024-07-28 20:15:36 -06:00
b2545e2f76 move copy icon for DIDs on contact screen 2024-07-28 17:53:09 -06:00
44ac98faa8 tweak verbiage and make other UI tweaks 2024-07-28 17:09:57 -06:00
d4cafd2f79 fix where it doesn't remove the plan when editing and removing it 2024-07-28 17:09:06 -06:00
de2b0e1940 fix problem detecting plans when editing gifts 2024-07-28 17:08:44 -06:00
361000e59b hide the details of a claim by default 2024-07-27 18:38:52 -06:00
ff35e53367 show full contact details, plus other tweaks 2024-07-27 18:22:22 -06:00
77ce5c8ca7 add a config for local testing, plus add mobile testing and some instructions 2024-07-27 16:52:44 -06:00
e8e5c70843 fix one linting error 2024-07-26 19:34:22 -06:00
4472c3fbdd import & update selected contacts 2024-07-26 19:12:12 -06:00
Jose Olarte III
0d73106d0e Playwright: check no-ID messaging 2024-07-26 18:51:28 +08:00
Jose Olarte III
cfb1906b5b Playwright: check test API 2024-07-26 18:16:20 +08:00
b742857940 remove example test file 2024-07-25 18:40:12 -06:00
Jose Olarte III
3d4babb280 Optimize tests 2024-07-25 16:53:33 +08:00
Jose Olarte III
c695bec8e3 Merge branch 'master' into test-playwright 2024-07-25 14:41:43 +08:00
9ca7363388 add help text, both in general and for download 2024-07-24 20:06:49 -06:00
44041cac92 enhance seed-backup with clipboard copy & more info 2024-07-24 19:23:25 -06:00
Jose Olarte III
8ce439f78a New test 2024-07-24 22:09:10 +08:00
3b772f8b4a make the list of all claims show a link to each specific claim 2024-07-23 20:58:19 -06:00
59820a2f01 add more type casts 2024-07-23 20:57:10 -06:00
d724d8093c add ability to edit a GiveAction 2024-07-23 20:14:07 -06:00
Jose Olarte III
71ef3718c8 More tests added 2024-07-23 21:26:05 +08:00
6456ce8dcc refactor out unused DB reference 2024-07-20 07:24:22 -06:00
5ad8a2d2ba await all of the db.settings updates 2024-07-20 07:19:27 -06:00
Jose Olarte III
190732fb00 Filename-based sequence 2024-07-20 17:03:57 +08:00
Jose Olarte III
cd3cbda801 Switched to baseURL 2024-07-20 16:36:16 +08:00
Jose Olarte III
72472e9d5e Simplify 2024-07-20 15:16:54 +08:00
Jose Olarte III
1fdb4bfe8c Check activity feed 2024-07-20 15:15:08 +08:00
Jose Olarte III
357b8df364 Cleanup 2024-07-20 14:55:55 +08:00
41a9c65afb fix linting 2024-07-19 21:15:56 -06:00
4e1df0eeee Merge branch 'passkey-cache' 2024-07-19 20:53:37 -06:00
4270374a67 create an identifier by default, while letting them choose if passkeys are enabled 2024-07-19 20:49:43 -06:00
9b9254cc13 Merge pull request 'docs: add tlmgr font packages' (#122) from kentbull/crowd-funder-for-time-pwa:kent/docs-update-tlmgr-packages into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#122
2024-07-19 20:00:06 -04:00
Kent Bull
2fb8601e3a docs: add tlmgr font packages 2024-07-19 17:59:54 -06:00
4272c45b9e rename "docs" directory to "doc" 2024-07-19 14:40:48 -06:00
47274a9e7c add instructions to run tests, and fix linting (for WebStorm) 2024-07-19 14:35:48 -06:00
b2ebc2992b cache the passkey JWANT access token for multiple signatures 2024-07-19 12:44:54 -06:00
41a33398b0 Merge pull request 'docs: basic pandoc setup' (#118) from kentbull/crowd-funder-for-time-pwa:kb/add-usage-guide into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#118
2024-07-19 12:47:18 -04:00
Jose Olarte III
27501f0898 Create Project automation + test 2024-07-19 19:21:48 +08:00
Jose Olarte III
1642f1e748 Cleanup 2024-07-19 17:40:22 +08:00
Jose Olarte III
6e82db7cff Updated test directory 2024-07-18 20:00:34 +08:00
Jose Olarte III
8702ad0d22 Tesdt: validate copy contact info to clipboard 2024-07-18 19:56:40 +08:00
Jose Olarte III
fdb2fae3b9 Test: new ID from seed phrase 2024-07-18 19:56:12 +08:00
Jose Olarte III
14c501d124 Playwright install 2024-07-18 19:55:57 +08:00
cd0a31e6f5 remove remaining getIdentity calls & fix QR code for did:peer 2024-07-15 20:47:10 -06:00
f7f38789d2 reword some things in help 2024-07-15 19:11:12 -06:00
f4f762b31c add BTC donation address 2024-07-15 17:18:22 -06:00
f6338c05ee move low-level DID-related create & decode into separate folder (#120)
Co-authored-by: Trent Larson <trent@trentlarson.com>
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#120
Co-authored-by: trentlarson <trent@trentlarson.com>
Co-committed-by: trentlarson <trent@trentlarson.com>
2024-07-13 13:24:54 -04:00
d1d6bf51b8 Merge pull request 'Refactor JWT-creation calls through single function' (#119) from passkey-all into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#119
2024-07-11 22:32:30 -04:00
f46a60b5dd change first page back to prompts without passkey 2024-07-11 19:54:20 -06:00
11163dfad9 consolidate getIdentity & remove dups 2024-07-11 19:43:56 -06:00
7cb9e2aa52 replace remaining didJwt.createJwt calls with one that checks for did:peer 2024-07-11 19:35:17 -06:00
145a1da37e linting cleanup 2024-07-09 19:42:55 -06:00
bce003e508 change accessToken to take a DID 2024-07-09 19:20:05 -06:00
45f0a14661 add expiration inside JWANT & refactor getHeaders to move toward supporting did:peer 2024-07-09 17:56:48 -06:00
42fde503e3 make a passkey-generator in start & home pages, and make that the default 2024-07-06 19:12:31 -06:00
6b65e31649 misc tweaks and linting clean-up 2024-07-06 13:04:15 -06:00
9677a344c2 misc syntactic & type-checking clean-up 2024-07-06 07:15:46 -06:00
e4a5629cff allow deletion of an identity 2024-07-05 19:37:45 -06:00
c4125822cb show a loading indicator on the claim-confirmation screen 2024-07-01 17:55:21 -06:00
6f2da589b1 fill in the "Load More" links for plan linkages 2024-06-30 20:10:18 -06:00
1ebfc997eb add section for gives provided by a plan 2024-06-30 20:06:47 -06:00
dea3f78173 fix type of the raw claim sent 2024-06-29 13:32:13 -06:00
053ee4a748 add advanced page & flag for editing raw claims, and fix recipient assignment in detail screen 2024-06-29 10:18:56 -06:00
acd5593c95 Merge branch 'master' into kb/add-usage-guide 2024-06-26 13:19:58 -04:00
Kent Bull
d4a9e7e364 docs: finish initial boostrapping dev guide 2024-06-26 11:06:18 -06:00
Kent Bull
91875e7305 docs: add more docs on local run 2024-06-25 19:30:29 -06:00
Kent Bull
abd751d562 docs: basic pandoc setup 2024-06-25 09:25:58 -06:00
9c7b138d06 modify & explain icons next to feed 2024-06-25 11:04:40 -04:00
b34e7daddf refactor display logic a bit (no flow changes intended) 2024-06-25 11:04:40 -04:00
4cb434fd5d passkey test (#116)
Co-authored-by: Trent Larson <trent@trentlarson.com>
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#116
Co-authored-by: trentlarson <trent@trentlarson.com>
Co-committed-by: trentlarson <trent@trentlarson.com>
2024-06-24 22:21:24 -04:00
1639e7cf25 move & resize the contact edit & info buttons 2024-06-22 12:34:30 -06:00
8f2bebe8ae bump version and add "-beta" 2024-06-22 12:23:57 -06:00
810f307442 bump to v 0.3.14 2024-06-22 12:23:10 -06:00
a4bdd2e922 fix checkbox verbiage when no project is chosen for a give 2024-06-22 12:06:55 -06:00
08e1ce6486 fix prompt for already-registered contacts (plus some verbiage) 2024-06-22 11:47:10 -06:00
e88eea7f36 add BX currency, add link for user's activity, tweak verbiage 2024-06-21 20:33:44 -06:00
ea156fac13 improve messaging when user has no offers or projects 2024-06-21 19:52:35 -06:00
a95d5db24a fix justification of checkboxes and text so they don't move 2024-06-21 19:25:46 -06:00
453256f874 give-detail page: add more-correct parameters from confirm-give page, and allow toggling of project & user-recipient 2024-06-21 19:13:19 -06:00
7bf488d4fe tweak UI for give-confirmation screen 2024-06-21 16:02:08 -06:00
230773a917 add Confirm Gift screen for simpler confirmation 2024-06-20 20:52:26 -06:00
79d93994c2 fix dependency vulnerabilities 2024-05-24 11:42:36 -06:00
bab4a62540 bump version and add -beta; enhance help 2024-05-24 10:21:08 -06:00
f84a2c2750 bump to verson 0.3.13 2024-05-24 10:19:17 -06:00
2321e1d6e8 allow link to the large version of a project image 2024-05-24 09:11:20 -06:00
af976ba838 add an image to projects (which shows on all ProjectIcons except for offers) 2024-05-23 20:51:40 -06:00
d08541fdae bump version and add -beta 2024-05-20 08:27:12 -06:00
fa92beed27 update CHANGELOG 2024-05-19 20:03:30 -06:00
9e1ae2abe5 bump to version 0.3.12 2024-05-19 19:57:02 -06:00
ad39ea05c2 fix the photo share_target, and tweak other verbiage 2024-05-19 19:56:25 -06:00
151c8154c4 bump version and add -beta 2024-05-19 19:32:38 -06:00
21a6348afc add a global error handler 2024-05-19 16:25:44 -06:00
210605c8e4 bump to version 0.3.11 (and enhance warning on profile deletion) 2024-05-19 08:39:18 -06:00
33a340326f set the correct active camera number when it starts 2024-05-17 20:24:33 -06:00
3f8596aacc bump version and add -beta 2024-05-17 12:16:23 -06:00
fd112bd447 allow any image URL for gifts & profiles 2024-05-12 21:43:18 -06:00
7d6b210ee1 allow file choice for gift, plus other UI fixes 2024-05-12 17:55:54 -06:00
6c28828c0a fix cropping problem where long images go off the screen 2024-05-12 12:39:16 -06:00
6af239378c bump to v 0.3.10, fix image upload on Chrome 2024-05-12 12:12:59 -06:00
4ff7d908d4 Merge pull request 'add a share_target for people to add a photo' (#115) from share-photo into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#115
2024-05-11 20:03:33 -04:00
17c901b1de add file-chooser to the profile image selection 2024-05-11 12:30:10 -06:00
f7b5dbf4ce style the sharing screen (plus other fixes) 2024-05-11 07:09:48 -06:00
7f02ba29a3 add a share_target for people to add a photo 2024-05-10 13:17:20 -06:00
20c4613533 increment version and add "-beta" 2024-04-28 20:10:39 -06:00
a44fc1d6d0 bump to version 0.3.9 2024-04-28 20:09:56 -06:00
b86543b404 disallow new-project page if not registered 2024-04-28 19:16:29 -06:00
7d0007e4d9 remove verbiage on front page that's now extra 2024-04-28 19:04:44 -06:00
ddd32e7f44 show something to indicate claims were sent (mostly in BVC screens) 2024-04-28 18:36:06 -06:00
8a9bb100ea constantly recheck on home screen if not registered 2024-04-28 17:02:31 -06:00
c48b8246f9 add registration inside contact import, with flag to hide it 2024-04-28 16:18:30 -06:00
b32a3d85e9 add 'registered' flag in contact info 2024-04-28 13:12:26 -06:00
8571c78a53 for scan on QR code screen, import and keep on that screen 2024-04-27 20:33:10 -06:00
eba68e2aaa add tweaks to testing instructions 2024-04-27 14:59:23 -06:00
e2df848e96 add page to view all claims about a DID (which we'll have to restrict to visible people soon) 2024-04-26 20:13:44 -06:00
9acba28b85 fix problem with duplicates in feed, plus some other UI tweaks 2024-04-26 17:05:11 -06:00
bef56fce10 allow loading more gives & offers & plans when limits are hit on project view 2024-04-26 15:44:09 -06:00
fccc4edb63 remove some 'uppercase' CSS markers 2024-04-25 20:17:49 -06:00
0a42edf595 put button directly on contacts page to show the given totals 2024-04-24 20:38:34 -06:00
f4f5fc7730 change remainder of "confirm" calls to better UX 2024-04-24 20:11:38 -06:00
eeaacaf202 replace many of the javascript "confirm" calls with the nicer UX version 2024-04-24 19:52:33 -06:00
d9aebfebd3 remove 'moment' library that's no longer used 2024-04-24 18:56:09 -06:00
7078f7b9e6 add choice of a start date for a project 2024-04-23 20:48:38 -06:00
d316f4924b add note about confirming your own, plus other helpful verbiage, plus notify messages that don't linger 2024-04-23 09:13:57 -06:00
1df2d3ed05 remove message confusion, add project name during give-details 2024-04-21 20:31:57 -06:00
4e877c15f6 change the "give" action on contact page to use dialog box 2024-04-21 16:42:22 -06:00
ef95708d02 add 'offer' on contact screen 2024-04-21 07:38:59 -06:00
7cbdc7a099 add code to display profiles in feed, but deactivate it for now 2024-04-20 19:53:11 -06:00
c748869c44 increment version and add "-beta" 2024-04-20 08:14:53 -06:00
60e11e23d4 bump to v 0.3.8 2024-04-20 08:06:34 -06:00
883687f1c3 make so cropping isn't behind header; delete profile image from storage when deleted 2024-04-19 20:13:44 -06:00
4466ceed99 Merge pull request 'profile-pic' (#114) from profile-pic into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#114
2024-04-19 17:36:53 -04:00
6d6e5266b4 make the home screen elements load more quickly 2024-04-19 15:37:10 -06:00
581a374b05 show contact's or user's icon in more places 2024-04-19 11:39:01 -06:00
1009574721 crop the image and store online and in settings 2024-04-18 20:27:43 -06:00
50cae65214 add photo to profile page (not yet saved) 2024-04-17 20:07:09 -06:00
48a46cf6f1 fix contact sorting to show those without names 2024-04-17 19:29:17 -06:00
60b2bf35fb update ClickUp link to a public link 2024-04-17 11:05:34 -06:00
cb5a7135ac remove tasks here in favor of ClickUp 2024-04-16 20:13:04 -06:00
a7a9e35766 note that tasks have moved 2024-04-11 20:43:52 -06:00
f029835e15 bump version and add "-beta" 2024-04-10 19:40:16 -06:00
017a172df3 bump to v 0.3.7 2024-04-10 19:32:46 -06:00
7837122a95 open the app when notification is clicked 2024-04-10 19:31:14 -06:00
0093255246 fix PWA creation & service-worker registration, plus some commentary tweaks 2024-04-09 20:29:21 -06:00
30bd53fb6f remove non-working interests, enhance error messages, update tasks & changelog 2024-04-09 17:54:17 -06:00
ca22930012 Merge pull request 'vitejs refactor' (#110) from jsnbuchanan/crowd-funder-for-time-pwa:feat/vitejs into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#110
2024-04-09 19:49:48 -04:00
c7c5bda014 Merge pull request 'misc tweaks for new vite build' (#4) from trentlarson/crowd-funder-from-jason:feat/vitejs-trent3 into feat/vitejs
Reviewed-on: #4
2024-04-09 06:05:55 -04:00
19aa572c95 misc tweaks for new vite build 2024-04-07 18:12:33 -06:00
03fae5dd95 Merge pull request 'A couple small fixes, plus a merge from master' (#1) from trentlarson/crowd-funder-from-jason:feat/vitejs-trent into feat/vitejs
Reviewed-on: #1
2024-04-07 13:52:43 -04:00
80818a8861 remove a lingering debug console.log 2024-04-07 11:39:13 -06:00
d29a8d9637 fix title of the test app 2024-04-07 11:32:53 -06:00
f0b0231515 add linting before any build 2024-04-07 11:22:20 -06:00
b73d2a3b58 fix linting 2024-04-07 11:02:54 -06:00
22cba5babf Merge remote-tracking branch 'original-origin/master' into feat/vitejs-trent 2024-04-07 09:41:14 -06:00
708ac51f23 avoid a huge error message in a likely-well-known scenario 2024-04-07 09:24:55 -06:00
a91ffc88b9 reorder home page vapid check to avoid an error on localhost 2024-04-07 09:16:42 -06:00
d727c2841b add missing Dexie import (which causes failure upon download click) 2024-04-07 09:13:32 -06:00
226a97732d on home page, change the filtered button color 2024-04-06 17:58:10 -06:00
c94dd7743b Merge pull request 'ui-additions-2024-03' (#113) from ui-additions-2024-03 into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#113
2024-04-06 19:46:16 -04:00
64e38cb8ff Merge branch 'master' into ui-additions-2024-03 2024-04-06 17:45:32 -06:00
e61ac31710 show in description when recipient is a project (not just Anonymous) 2024-04-06 17:39:40 -06:00
3fbf68b117 filter by selections (now all working), add cache for plans 2024-04-06 14:01:18 -06:00
d4390483d9 Merge pull request 'send a time for notifications to the push server' (#112) from notify-time into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#112
2024-04-03 22:04:36 -04:00
8dea2091af Merge pull request 'ui-fixes-2024-03' (#111) from ui-fixes-2024-03 into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#111
2024-04-03 22:04:02 -04:00
e3696e3ac5 feed filter: save the changed values to the DB, go to map if no location chosen, reload if necessary 2024-04-03 19:54:01 -06:00
Jose Olarte III
027825b155 Names and variables for filter toggles 2024-04-03 15:51:23 +08:00
911203c190 adjust more code to the PushSubscriptionJSON 2024-04-02 19:32:41 -06:00
2da0394003 adjust the notification-subscription objects to try and send correct info 2024-04-02 19:18:31 -06:00
4a65d095db add adjustment to UTC hour for notification time 2024-04-02 18:38:44 -06:00
8ea5779312 update tasks 2024-04-01 19:06:18 -06:00
144ab76716 add logic to send a time for notifications 2024-04-01 19:04:54 -06:00
Jose Olarte III
8da2c8cc30 Additions to Account View 2024-03-29 21:41:14 +08:00
Jose Olarte III
570b31e2d6 Removed one more 2024-03-29 15:55:16 +08:00
Jose Olarte III
07f542ca16 Filter options reduced for release 2024-03-29 15:53:46 +08:00
Jose Olarte III
62e0fc51c2 Feed filters dialog 2024-03-27 19:57:31 +08:00
Jose Olarte III
94b600e527 Map fix #2 2024-03-26 21:38:21 +08:00
Jose Olarte III
5388e6052c Button width changes
For buttons that are next to each other
2024-03-26 19:55:16 +08:00
Jose Olarte III
21fe5a0279 Optimized grid space for wider screens 2024-03-26 17:12:55 +08:00
Jose Olarte III
ffba89a7b5 Fixed map z-index 2024-03-26 16:54:43 +08:00
Jose Olarte III
31954d2690 Added close icon to gifted prompts dialog 2024-03-26 16:02:24 +08:00
340d0a5219 refactor tasks 2024-03-25 19:03:01 -06:00
2d2785d6a0 docs: adding do for updated development server run command
- `npm run dev`
2024-03-25 08:15:04 -06:00
41d6e5fc73 fix: buffer typescript error in util.ts when parsing ArrayBuffer 2024-03-25 08:10:38 -06:00
7412d67c33 bump version and add -beta 2024-03-24 19:04:24 -06:00
83db5302ad bump to version 0.3.6 2024-03-24 18:28:42 -06:00
75f9f20ea3 fix check for more camera-device options 2024-03-24 18:27:06 -06:00
e43c45ebea add onboarding help instructions as separate page 2024-03-24 17:01:53 -06:00
708032311a Merge pull request 'add button during photo to switch to mirror mode' (#109) from photo-reverse into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#109
2024-03-24 18:59:50 -04:00
5dead960ae fix: es modules syntax for buffer deps instead of commonjs require 2024-03-24 13:05:22 -06:00
12d81b79c7 chore: update vitejs config to deploy on the same default port as the @vue/cli-service
This port is 8080. This is done to not break existing tooling and devops code.
2024-03-24 12:05:09 -06:00
f3dc81e6eb fix: AccountViewView.vue template not resolving dep for dexie-export-import/dist/import
Previous error:

Error: The following dependencies are imported but could not be resolved:

  dexie-export-import/dist/import (imported by /Users/jason/dev/src/trent/crowd-funder-for-time-pwa/src/views/AccountViewView.vue?id=0)

Are they installed?
    at file:///Users/jason/dev/src/trent/crowd-funder-for-time-pwa/node_modules/vite/dist/node/chunks/dep-DJaaTb_D.js:52506:23
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async file:///Users/jason/dev/src/trent/crowd-funder-for-time-pwa/node_modules/vite/dist/node/chunks/dep-DJaaTb_D.js:51972:38
2024-03-24 11:51:58 -06:00
ef5f81932d Initial stab at vitejs update 2024-03-24 11:18:29 -06:00
214a264179 change icon for detail view (from circle-info to file-lines) 2024-03-23 19:07:53 -06:00
9b183a4b6c add blurb explaining what data is shared with the world 2024-03-23 18:45:26 -06:00
f365cc9e3c show warnings before dismissing prompt, and add to tasks and help 2024-03-23 17:35:58 -06:00
9059f7a9a7 add button on photo to switch to mirror mode 2024-03-23 16:31:23 -06:00
e6cd86618e bump version to 0.3.5 2024-03-23 02:41:25 -06:00
c3fd27b140 fix so that project agent & location removals get saved 2024-03-23 02:31:44 -06:00
cf2e800dec add a camera-switch button 2024-03-23 01:32:55 -06:00
b60383cfe9 Merge pull request 'fix: npm audit fix to resolve vulnerabilities 1 low, 3 moderate, 1 high' (#108) from jsnbuchanan/crowd-funder-for-time-pwa:master into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#108
2024-03-22 12:01:21 -04:00
c7d93db6f2 deps: npm audit fix to resolve vulnerabilities 1 low, 3 moderate, 1 high
There are still 9 moderate severity vulnerabilities, but I will work on those independentally because they may involve updating to library version that have breaking changes.
2024-03-22 09:29:42 -06:00
5e771e4a24 refactor tasks 2024-03-22 07:12:24 -06:00
4dd2c044d5 bump to v 0.3.4 2024-03-21 06:56:35 -06:00
3bfd54362e Merge pull request 'Sample button visual enhancement' (#104) from button-visual-enhancement into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#104
2024-03-21 07:57:18 -04:00
Jose Olarte III
b6e344a15e Propagated button improvements across views 2024-03-21 19:30:42 +08:00
Jose Olarte III
3d1c46aef8 Merge branch 'master' into button-visual-enhancement 2024-03-21 15:40:54 +08:00
ce05f7d003 Merge pull request 'tweak imagery so that it doesn't get stretched on a mobile device' (#107) from photo-ratio into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#107
2024-03-20 20:26:35 -04:00
313cd79e60 finalize the photo-taking code, adding comments and removing logging 2024-03-20 18:23:57 -06:00
121991b53a Merge branch 'gifted-camera-improvements' into photo-ratio 2024-03-20 16:54:39 -06:00
Jose Olarte III
cbf8cb9f46 Fixed placement of upload/retry buttons 2024-03-20 21:11:01 +08:00
Jose Olarte III
fe0668e4b3 Improved Camera Popup 2024-03-20 19:17:18 +08:00
a230506d96 change the X and picture button so that landscape is all functional (if not great-looking) 2024-03-19 21:02:57 -06:00
c49c55d394 change the photo ratios to fix all but portrait-orientation on mobile-emulation 2024-03-18 22:20:02 -06:00
ae572afff6 add help for when service workers get stuck; bump to version 0.3.2 2024-03-17 20:20:13 -06:00
ccea2486e4 change build for test servers, bump version to 0.3.1 2024-03-17 16:42:49 -06:00
155343a9d7 bump to v 0.3.0 2024-03-17 10:49:28 -06:00
85ad295eb9 Merge pull request 'photo-upload' (#105) from photo-upload into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#105
2024-03-17 12:23:32 -04:00
64322b2804 change the default image server port 2024-03-17 10:22:05 -06:00
3e556dfa52 move the "part of project" text in giving-details screen 2024-03-17 08:53:14 -06:00
252952e017 show the image rate limits 2024-03-15 16:42:25 -06:00
251986d2bc make the photo show in a pop-up dialog 2024-03-14 19:44:11 -06:00
49bb1c07b7 fix file extension 2024-03-14 08:46:28 -06:00
67f34f9826 on "give details" page, distinguish between project & user destination 2024-03-10 17:49:53 -06:00
476d35452a send the claim type with an image 2024-03-10 17:37:10 -06:00
26582030df add another check when deleting an image 2024-03-10 17:36:49 -06:00
ae857f4c8f guard against another set of errors when deleting an image 2024-03-10 16:46:54 -06:00
c602c5ce50 add some other image deletions in other cases 2024-03-10 14:53:41 -06:00
e4543457e2 add image onto give claim, then display on feel (full round-trip, baby!) 2024-03-09 19:37:14 -07:00
c58f012d2c allow viewing and deletion of an image 2024-03-08 23:56:19 -07:00
792e9cb648 separate picture taking from uploading 2024-03-08 09:54:10 -07:00
acee761906 add page for extended details of gifts including pic (not fully tested) 2024-03-08 01:10:17 -07:00
cae2bbc4ff make styled button to take picture 2024-03-07 09:09:51 -07:00
Jose Olarte III
a5c3600673 Sample button visual enhancement 2024-03-07 18:57:08 +08:00
0eb64ed716 add authentication token for image server, change default image server to localhost 2024-03-06 06:12:41 -07:00
f1bb1b51aa Merge branch 'master' into photo-upload 2024-03-05 20:25:41 -07:00
92b924643e fix camera resolution, parameterize image API server 2024-03-05 20:20:54 -07:00
ca90447700 fix different "environment" variables for prod & dev 2024-03-02 16:15:03 -07:00
750700e75e bump version and add '-beta' 2024-03-01 15:59:38 -07:00
3612ea4224 bump to v 0.2.17; add "personalized" message and better confirmation-result messages 2024-03-01 15:54:50 -07:00
dbccbf7e4a fix: show on the confirmation page when there are hidden claims 2024-03-01 14:40:51 -07:00
1258cf02a1 bump to v 0.2.15 2024-03-01 14:06:01 -07:00
a488a36bc0 Merge pull request 'Shortcut page for BVC assertions & confirmations' (#103) from bvc-shortcut into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#103
2024-03-01 15:14:01 -05:00
a93b556e0c doc: refactor tasks 2024-02-26 19:43:23 -07:00
2c28913d97 for BVC: finish submission of confirmations & final give 2024-02-26 19:27:34 -07:00
0b24d7bbd8 for BVC: fix the attendee & show appropriate success message 2024-02-25 18:55:58 -07:00
2058205150 for BVC shortcut: send attend & give actions, and list actions to confirm 2024-02-25 18:38:54 -07:00
866dcb3a2a add screens for the shortcuts for the BVC group (doesn't submit yet) 2024-02-24 18:38:11 -07:00
6aab1ff49d consolidate interface and remove copies of code 2024-02-24 10:26:12 -07:00
c696de33f3 add page to take a picture and upload to an image server 2024-02-23 19:02:10 -07:00
c239db6a4f doc: update tasks 2024-02-19 19:44:59 -07:00
3eda5f6b5d show more succinct info in feed, targeted toward user's visibility 2024-02-19 19:43:55 -07:00
783b38df65 order contacts by name & note outside network as "outside your network" 2024-02-18 14:58:10 -07:00
3475c32e1f update onboarding hint message, justify text on QR page 2024-02-17 12:55:30 -07:00
dcd881adae make the name-setting prompt yellow 2024-02-17 12:46:17 -07:00
37690cc855 increment versiona and add "-beta" 2024-02-14 20:56:37 -07:00
5f9edea116 bump version to 0.2.14 2024-02-14 20:50:15 -07:00
f517b09ed7 combine all service-worker scripts into a single file to try and ensure included scripts aren't lost 2024-02-14 20:46:34 -07:00
ca70b19831 fix claim-view page when the claim argument is not a global ID 2024-02-12 20:10:18 -07:00
f41e541fe2 send the last JWT instead of the identifier for plan edits 2024-02-11 16:05:15 -07:00
5c547783a7 remove unused page; tweak task list 2024-02-11 07:14:16 -07:00
8d2dd6357a update readme 2024-02-09 09:08:26 -07:00
189261e991 update messaging for contact registration 2024-02-07 18:57:58 -07:00
15464602f9 bump to version 0.2.14-beta 2024-02-07 18:42:33 -07:00
331c4f64d6 add check for valid "did:" DIDs 2024-02-07 18:23:13 -07:00
28ae317958 refactor tasks & add more estimates 2024-02-05 09:03:55 -07:00
643718619e remove unnecessary logic in account switcher; refactor task list 2024-02-04 20:11:04 -07:00
c3819ec919 don't autocapitalize website input; refactor tasks 2024-02-03 19:33:52 -07:00
719e3a467d make a number input targeted towards numbers 2024-02-03 19:21:07 -07:00
b251d7e4fd change project icon to a hammer 2024-02-03 19:20:54 -07:00
61c3a0e30b avoid error on browsers without a service worker 2024-02-03 19:19:58 -07:00
a76df55224 add display of my own offers 2024-02-03 18:56:09 -07:00
e140da081f fix name derivation on give dialog 2024-02-03 18:10:46 -07:00
1be899c48d ensure error message shows, and unset register flag if there's an API error 2024-02-02 17:40:06 -07:00
6aee93ca6c update tasks; enhance an error message & some typescripts 2024-02-02 12:25:04 -07:00
5412625d05 increment version and add -beta; tweak tasks & tests 2024-02-02 10:22:51 -07:00
8f579b40a9 bump to verson 0.2.12 2024-02-01 12:12:13 -07:00
e8a907c63a add more thankfulness prompts 2024-02-01 12:09:09 -07:00
f53a6f3045 tweak the prompt for contacts to be able to skip them 2024-02-01 11:52:31 -07:00
b38ebc45e1 add a prompt for things for which to express gratitude 2024-01-31 21:15:40 -07:00
c51d2629b3 bump version and add -beta 2024-01-28 15:20:15 -07:00
e642b99ff5 bump version to 0.2.11 2024-01-28 15:04:12 -07:00
26f1e88f5a doc: update tests & tasks 2024-01-28 15:01:47 -07:00
2e164dfeff tweak messages for missing identifier 2024-01-28 13:21:21 -07:00
d7530ff56b adjust more UI on the Advanced section, and make other small code & UI tweaks 2024-01-27 17:32:17 -07:00
2db52cb72e fix default server display in advanced section & refactor UI 2024-01-27 14:49:51 -07:00
c8eb3bfbc0 move save & cancel buttons further apart 2024-01-27 14:15:14 -07:00
71b210d541 add to manual tests & changelog 2024-01-27 13:05:33 -07:00
66289ec206 update tasks 2024-01-27 13:02:47 -07:00
639dc7b4e5 add instruction to error output 2024-01-27 12:40:40 -07:00
4fe072f19e move DB logic out of 'created' in components since it's not needed yet 2024-01-27 08:27:52 -07:00
f253f0af0f add ability to import from Endorser Mobile CSV 2024-01-26 20:36:29 -07:00
2d95a35905 add date to project give record list; don't wrap icon & amount 2024-01-26 16:10:14 -07:00
88f869d600 lower project "I Gave" button into list of contact, and tweak other wording 2024-01-24 20:46:05 -07:00
a0911bb0fd add copy-paste icon next to non-anonymous, non-hidden DIDs on details page 2024-01-21 15:50:39 -07:00
1053b78ab8 add sharing & copying instructions when asking contacts for help, and list all the visibleTo DIDs with an English description of their path 2024-01-21 15:16:39 -07:00
dcfa8d9451 add first stab at showing how the contact is visible in my network 2024-01-20 20:33:51 -07:00
dd38f76ee1 increment version and add -beta; add to tasks and tests 2024-01-18 21:11:19 -07:00
667e1e8890 bump version to 0.2.10 2024-01-18 20:05:44 -07:00
1731f2443b update offer dialog to allow other units 2024-01-17 20:50:35 -07:00
e1cffcda2d fix problem where extended screen of contacts didn't pass project 2024-01-17 20:18:01 -07:00
a5b1b97012 show the identicon in large size on the contacts screen 2024-01-17 19:47:33 -07:00
563b5793a9 add different identicons for people (and increment version & add -beta) 2024-01-17 19:27:05 -07:00
660436c8fa add copy-did-to-clipboard on contact list 2024-01-16 19:58:18 -07:00
31a7752168 add link to project from gives on front page 2024-01-16 19:48:47 -07:00
3ebe7bc156 put didInfo names in more places and add copy icons for DIDs & IDs 2024-01-16 18:58:08 -07:00
0eb16d5661 add links for give & offer when they fulfill other things 2024-01-16 17:52:32 -07:00
edb09da10f add detailed-info button for a project 2024-01-16 15:31:55 -07:00
be6ec6745a show a 'give' button directly on offers in the ProjectView 2024-01-16 15:23:40 -07:00
b79c5fcf91 move info button for offer & add cursor for hover 2024-01-15 19:50:00 -07:00
9dea4066c9 add ability to confirm give directly from a project 2024-01-15 19:40:38 -07:00
9b586566f0 increment version and add "-beta" 2024-01-15 12:37:39 -07:00
e5e702f8a5 bump version to 0.2.9 2024-01-15 12:14:15 -07:00
32c9076c39 fix visibility after adding contact, and some messaging 2024-01-15 12:06:33 -07:00
6ab4c40fd0 bump to version 0.2.8 2024-01-14 21:03:22 -07:00
d7ef07c2e2 automatically create an identity on the first page (and other UI tweaks) 2024-01-14 21:00:59 -07:00
9f595040d8 fix problem with anonymous contributor; refine tasks 2024-01-14 15:27:57 -07:00
40a8794649 remove checks on old fullIri field 2024-01-13 18:48:29 -07:00
fa72d38d18 allow an agent to edit a project 2024-01-13 18:45:51 -07:00
31aacb286f reword prompt for creating an identifier on the start screen 2024-01-13 18:44:59 -07:00
2511f18fa7 enhance (& fix for mobile) styling and verbiage 2024-01-13 15:14:16 -07:00
febfa8b098 bump version to 0.2.7 2024-01-12 20:54:29 -07:00
e0fcb1f67b fix various verbiage 2024-01-12 20:40:46 -07:00
9183092325 fix the name of the offerer 2024-01-12 20:39:30 -07:00
a87179d127 change wording from "identity" to "identifier" in many places 2024-01-12 16:37:02 -07:00
14e203dd74 bump to version 0.2.6 2024-01-12 15:58:08 -07:00
acaaf8776d add ability to give to fulfill an offer; adjust visibility of claim actions 2024-01-12 15:54:45 -07:00
cb1f38c182 bump to verson 0.2.5, and edit tasks 2024-01-09 19:32:18 -07:00
cfa7466b94 show users when there's an error on the import page 2024-01-09 19:18:49 -07:00
f998364c72 update package-lock for previous bump 2024-01-09 18:31:53 -07:00
7b4f084b4b bump to version 0.2.4, update tasks 2024-01-09 18:31:11 -07:00
115329e26c update the onboarding help blurb 2024-01-09 18:30:55 -07:00
61bef57563 update some dependencies with moderate severity 2024-01-09 17:56:27 -07:00
a5368d0f82 update library with vulnerability 2024-01-09 17:54:37 -07:00
48cb45d230 bump version to 0.2.3, add endpoint name update 2024-01-09 17:49:41 -07:00
8a7ce0fe65 add flag for logging a contribution as a trade 2024-01-08 21:28:04 -07:00
525d3fc15a bump to version 0.2.2 2024-01-05 13:54:57 -07:00
68f3b79983 add hints for registration on the contact page 2024-01-05 13:50:35 -07:00
5353fe770a tweak verbiage and usability 2024-01-05 13:08:20 -07:00
60fec5763d bump version to 0.2.1 2024-01-05 12:48:38 -07:00
aeb1d6a6a5 add next-public-key-hash to manual input 2024-01-05 12:44:28 -07:00
ec6175a550 make a confirmation for contact visibility 2024-01-05 12:14:56 -07:00
c1361e088f render full claim details in a more resonable format of YAML not JSON 2024-01-05 11:20:16 -07:00
a2c986951e doc: refactor project tasks 2024-01-05 11:11:06 -07:00
dce7b8e3d9 add terms & conditions, and a note about data in this service 2024-01-05 10:34:13 -07:00
211e0487fe add verbiage for other non-Chrome cases 2024-01-05 09:56:29 -07:00
cc931dcb04 add notification check with instructions on front screen 2024-01-05 09:48:15 -07:00
bfe14cc9c2 increment version and add -beta 2024-01-04 10:40:15 -07:00
275dba4468 bump version to 0.2.0 2024-01-04 10:35:32 -07:00
1f05e81b05 add notification immediately after setting up subscription, and tweak messaging 2024-01-03 21:27:13 -07:00
e9ad68f2a5 add maskable images 2024-01-03 18:32:38 -07:00
934664b9c9 add the hashed-next-key to the contact data, shown & stored 2024-01-03 17:44:41 -07:00
780be59c76 remove name from identity switcher (since they are not tied to a DID) 2024-01-02 19:53:19 -07:00
4a0bedb628 fix one more list-outside indent 2024-01-02 19:27:13 -07:00
5689f95230 change list-inside to list-outside 2024-01-02 19:21:57 -07:00
3083bb084a add more notification help instructions; remove confusing, big name-edit button 2024-01-02 19:16:54 -07:00
821d27a58a Merge pull request 'Set max screen content width' (#102) from app-screen-max-width into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#102
2024-01-02 10:24:09 -05:00
Jose Olarte III
998a1d312f Set max screen content width 2024-01-02 15:25:51 +08:00
1f13bf772c move wait for service-worker initialization into the notification modal 2024-01-01 20:36:23 -07:00
def744b3df don't allow notification service-worker interaction until it is ready 2024-01-01 20:04:37 -07:00
0fb37acb24 increment version and add -beta 2024-01-01 19:38:07 -07:00
20bb723f0b bump to version 0.1.9 2024-01-01 19:34:34 -07:00
d821a7bd59 Merge pull request 'make a backup download for browsers that don't get it automatically' (#101) from another-download into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#101
2024-01-01 21:28:22 -05:00
9f3b7314e8 Merge branch 'master' into another-download 2024-01-01 19:27:52 -07:00
4df7bb58a4 add help for clearing data, plus some other help fixes 2024-01-01 17:09:30 -07:00
15ccd2394f add missing 'date' to log interface 2024-01-01 16:25:23 -07:00
920c7bb612 adjust look & message on DB download 2024-01-01 16:24:36 -07:00
6eb26ea90c remove IndexedDB keys that shouldn't be keys, and remove unused table, and add commentary 2024-01-01 16:24:30 -07:00
28b6d9bbf9 doc: reorganize top tasks 2023-12-31 19:58:51 -05:00
7a099183ae remove db.close calls that caused an error when trying to download 2023-12-29 13:14:35 -07:00
11070755d6 refine error message when duplicate contact is input 2023-12-29 13:10:07 -07:00
c79dfac1fe add a way to import contacts & settings 2023-12-29 12:46:47 -07:00
2b06c64664 make a backup download for browsers that don't get it automatically 2023-12-28 14:42:56 -07:00
769a928b3d increment version & add "-beta" 2023-12-27 20:03:59 -07:00
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
92fcffdfc5 update the script commands for JWT signature generation & validation 2023-11-17 20:33:57 -07:00
5f5562f5e3 doc: update tasks 2023-11-17 17:47:41 -07:00
74ed025377 move search area selection out of discover onto it's own screen 2023-11-17 15:23:48 -07:00
f36ecfd8db misc look-and-feel tweaks 2023-11-17 13:54:32 -07: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
fc70a11bd8 Merge pull request 'allow access to a different project by link' (#79) from project-ids into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#79
2023-11-12 05:12:30 -05:00
73f890beac modify English verbiage for push-server instructions 2023-11-11 22:26:10 -07:00
67dce9e678 allow for a project ID in the URL 2023-11-11 22:25:06 -07:00
2b66ddfb83 update CHANGELOG 2023-11-11 13:25:05 -07:00
56fc2893a2 fix version in package-lock; modify a project task 2023-11-11 13:11:40 -07:00
Matthew Raymer
552ad5a267 Two static scripts for push notifications 2023-11-08 06:02:07 -05:00
Matthew Raymer
910f57ec7d Update version 2023-11-08 05:53:50 -05:00
Matthew Raymer
e813315dad Fixes and updates 2023-11-08 05:52:29 -05:00
aea9626c06 Merge pull request 'web-push-permissions' (#67) from web-push-permissions into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#67
2023-11-08 01:49:49 -05:00
Matthew Aaron Raymer
7f0f1b7fc8 Merging changes 2023-11-08 14:47:48 +08:00
Matthew Raymer
cfc4d0a947 Updates package file 2023-11-07 21:12:49 +08:00
Matthew Raymer
8684488def Stip out old approach 2023-11-07 08:04:26 -05:00
Matthew Raymer
a820a7b131 Minor formatting changes 2023-11-07 21:01:37 +08:00
30d45c0acf tweak some more tasks (so it's clear which ones require UI expertise) 2023-11-06 18:07:57 -07:00
221bb2a27c move other route into alphabetical order (no logic changes) 2023-11-06 18:04:35 -07:00
2961e29831 move routes into alphabetical order (no logic changes) 2023-11-06 09:09:34 -07:00
5ae5e110c2 refine task list 2023-11-06 09:08:56 -07:00
20c2954be1 remove unused properties 2023-11-06 08:59:31 -07:00
a848e1fa81 Merge pull request 'consolidate into GiftedDialog because the result was always the same' (#76) from more-smalls into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#76
2023-11-06 07:56:07 -05:00
85bd807bcc allow view of feed without an identifier 2023-11-05 21:46:46 -07:00
eeece8a1b4 refactor UI & remove unused code on account screen 2023-11-05 18:15:52 -07:00
bbfc1e1007 modify naming for unnamed gifter 2023-11-05 18:05:59 -07:00
433d0c023e refactor look of identity page 2023-11-05 17:55:03 -07:00
ac6376243b refactor name setting and other small messaging & types 2023-11-05 17:30:23 -07:00
a12f033b72 refactor type that was a duplication 2023-11-05 17:02:24 -07:00
42cd7d00de fix message to user 2023-11-05 17:02:05 -07:00
c388cc8cfe remove lastName and just have a single name field 2023-11-05 16:34:18 -07:00
6d4d4e40c3 Merge branch 'master' into more-smalls 2023-11-05 15:55:13 -07:00
3b39faf173 fix linting error 2023-11-05 15:51:47 -07:00
f43ecc98aa tweak notifications on errors and other responses 2023-11-05 08:29:36 -07:00
5b7ccf9ef0 fix where the project ID was not included; fix the pause when submitting give & show toast of aknowledgement; remove 'emit' 2023-11-05 07:46:16 -07:00
9bacd4da87 consolidate into GiftedDialog because the result was always the same 2023-11-04 18:48:31 -06:00
Matthew Raymer
ee28b18b14 Added message handler 2023-11-04 08:46:48 -04:00
7450d8d1c3 Merge pull request 'more miscellany' (#75) from misc2 into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#75
2023-11-04 00:14:20 -04:00
7490cfc557 Merge branch 'master' into misc2 2023-11-03 22:12:31 -06:00
95287e4dd0 Merge pull request 'add IDs for puppeteer test script' (#74) from ids-for-tests into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#74
2023-11-03 21:50:08 -04:00
679d1a70e8 Merge pull request 'allow edit of a contact's name' (#73) from contact-edit into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#73
2023-11-03 21:49:40 -04:00
047fb263dd Merge pull request 'a couple of fixes' (#72) from fixes into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#72
2023-11-03 21:49:12 -04:00
b76cf28bc2 Merge pull request 'edit text on the help page' (#70) from misc into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#70
2023-11-03 21:47:45 -04:00
58c091cdaa add test view to hold testing functionality 2023-11-03 16:48:34 -06:00
0df5a975f3 add bottom navigation to edit-plan page 2023-11-03 16:17:25 -06:00
94051e6ba9 revert changes that are contained in other PRs 2023-11-03 13:27:50 -06:00
8e60f53f0b fix name of class to match the file name (and avoid clash with existing class) 2023-11-03 13:23:11 -06:00
afc48a5434 add IDs for puppeteer test script 2023-11-03 13:19:42 -06:00
6eb3381a98 enhancements to contact name edit 2023-11-03 10:24:48 -06:00
2bec218cc5 allow edit of a contact's name 2023-11-02 20:50:33 -06:00
327c655fb3 add a couple more tasks 2023-11-02 09:15:21 -06:00
866aad069f add multiple new tasks based on board meeting feedback 2023-11-02 09:04:42 -06:00
7f6c938029 fix display of given items; bump version to 0.1.2 2023-11-01 09:09:20 -06:00
6d2df4a50c fix the error message on a successful result 2023-11-01 08:56:22 -06:00
7305606546 improve give descriptions, bump to v 0.1.1 2023-10-31 20:53:39 -06:00
2a9ff8aa77 add some instructions and other tasks 2023-10-31 20:19:21 -06:00
829994491c improve messages for feed summaries 2023-10-31 19:58:37 -06:00
ce06e8f0fa update derivation path to the one for time 2023-10-31 19:58:07 -06:00
1ee751eea8 Merge pull request 'add QR scanner for adding a contact' (#69) from qr-reader-rebased into master 2023-10-31 10:34:59 -04:00
Matthew Raymer
2d38183dce Some updates and nudging toward notification ui 2023-10-16 07:43:10 -04:00
Matthew Raymer
082a6eae1f Refactor db setup a bit 2023-10-16 18:49:18 +08:00
Matthew Raymer
d07fb47721 Debugging 2023-10-04 22:16:58 -04:00
Matthew Raymer
ccb6160bca Updates to additional scripts 2023-10-02 04:41:50 -04:00
116b239616 change derivation path, add task to test app updating 2023-09-15 15:00:33 -06:00
Matthew Raymer
2eaa4203aa Debugging and ironing out the flow 2023-09-12 08:03:03 -04:00
Matthew Raymer
f27a18c712 Fix db path; add new pathing for web-push; load VAPID at boot 2023-09-11 07:51:59 -04:00
f47346cc35 edit text on the help page 2023-09-09 08:13:55 -06:00
Matthew Raymer
2c4a920c3c Updates for web push workflow 2023-09-08 21:04:50 +08:00
0e02268950 update tasks 2023-09-07 19:36:49 -06:00
94d9c425ad add QR scanner for adding a contact 2023-09-07 18:59:19 -06:00
Matthew Raymer
ed91cadd9d Added permission step; UI workflow documentation 2023-09-07 20:42:15 +08:00
Matthew Raymer
a6de282aec Another upgrade of package.json 2023-09-07 18:31:55 +08:00
2db662c125 Merge pull request 'New branch for cleanup and web push' (#65) from new-web-push into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#65
2023-09-07 06:13:45 -04:00
b7892f4dfa Merge branch 'master' into new-web-push 2023-09-07 06:12:56 -04:00
Matthew Raymer
3bbb138299 Merge branch 'new-web-push' of ssh://173.199.124.46:222/trent_larson/kick-starter-for-time-pwa into new-web-push 2023-09-07 18:09:50 +08:00
Matthew Raymer
5b5c631001 Fix typing errors from the refactoring 2023-09-07 18:07:53 +08:00
Jose Olarte III
e60b56a0b0 Added notification dialog workflow description 2023-09-07 18:04:00 +08:00
Jose Olarte III
d3e025c293 Dialog test suite additions 2023-09-07 18:03:46 +08:00
Jose Olarte III
6f4027f614 Notification UI changes to accommodate mute 2023-09-07 18:03:33 +08:00
249811efe3 Merge branch 'master' into new-web-push 2023-09-07 02:35:20 -04:00
bd2455458f Merge pull request 'mostly removing the "Anonymous" word and associated graphic' (#66) from miscellany into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#66
2023-09-07 02:35:09 -04:00
a053c48819 make the Anonymous icon the same everywhere, and tweak more in the task list 2023-09-06 20:06:11 -06:00
9486142b2a make the Anonymous icon to be blank, plus some other renaming & task cleanup 2023-09-06 19:48:47 -06:00
Matthew Raymer
2fba7f2a55 Merge branch 'new-web-push' of ssh://173.199.124.46:222/trent_larson/kick-starter-for-time-pwa into new-web-push 2023-09-06 20:49:23 +08:00
Matthew Raymer
31d13b9143 Refactoring for cleanup 2023-09-06 20:46:16 +08:00
Matthew Raymer
852bd93f3f New branch for cleanup and web push 2023-09-05 21:25:04 +08:00
b707bfce40 Merge branch 'master' into new-web-push 2023-09-05 08:25:28 -04:00
bdb8e2e32a Merge pull request 'assign boundaries for local search' (#63) from search-bbox into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#63
2023-09-05 07:45:23 -04:00
06b173e861 fix bad prompt if they choose to confirm, and use the right phrasing for singular "hour" 2023-09-04 19:55:58 -06:00
6a8b9d36a7 move limit checks out of "advanced" section, and always allow registration (for tests) 2023-09-04 19:03:45 -06:00
52a6451a2d remove technical details from user-facing messages 2023-09-04 15:37:39 -06:00
4b9cbd0e9f fix all the lint warnings 2023-09-04 15:17:32 -06:00
a5e0c847b1 fix the last of the type annotations (still have to fix no-explicit-any warnings) 2023-09-04 08:03:43 -06:00
Matthew Raymer
fd43da93a5 A whole lot of cleaning going on 2023-09-04 20:44:00 +08:00
b59bcf249a fix many more typescript errors 2023-09-03 21:40:40 -06:00
b05b602acd fix many, many more type errors 2023-09-03 10:02:17 -06:00
b8aaffbf8d commit the recreated package-lock.json file 2023-09-03 10:01:51 -06:00
Matthew Raymer
5501ac1a2f Many fixes -- especially and endorserServer 2023-09-03 21:08:30 +08:00
Matthew Aaron Raymer
b514d64068 Type fixes 2023-09-02 18:15:30 +08:00
Matthew Aaron Raymer
c4537420b4 Merge branch 'search-bbox' of ssh://173.199.124.46:222/trent_larson/kick-starter-for-time-pwa into search-bbox 2023-09-02 17:09:58 +08:00
Matthew Aaron Raymer
5f50338dd0 We have to remove the decorator in package -- it breaks install 2023-09-02 17:09:41 +08:00
308386d829 Merge branch 'master' into search-bbox 2023-09-02 04:08:40 -04:00
999d7abc04 fix linting 2023-09-01 13:14:40 -06:00
f7f947bfdd finish selection of a location on the nearby search -- it all works :-) 2023-08-31 20:55:51 -06:00
26d9b134c7 refactor map selection, and now location selection & cancellation works (but not saving yet) 2023-08-31 18:58:34 -06:00
43f942c905 Merge pull request 'move ID switcher to advanced section' (#62) from move-id-switch into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#62
2023-08-30 02:43:57 -04:00
8ee610c1bc start the assignment of boundaries for a local search 2023-08-29 20:47:22 -06:00
8d15b7bfb8 Merge branch 'master' into move-id-switch 2023-08-28 08:20:33 -06:00
5c57ee3e72 move ID switcher to advanced section 2023-08-28 08:18:02 -06:00
Matthew Raymer
3f7bcbfd76 Added unsubscribe and mute 2023-08-28 19:57:47 +08:00
ef0988c9ec Merge pull request 'add link to map on projects which have a location' (#61) from project-map-link into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#61
2023-08-28 01:30:25 -04:00
22de6113e9 Merge branch 'master' into project-map-link 2023-08-28 01:27:30 -04:00
87139f203c Merge pull request 'fix some formatting of web-push doc' (#60) from plan-loc into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#60
2023-08-28 01:26:45 -04:00
c8de13d376 add link to map on projects which have a location 2023-08-27 20:20:16 -06:00
156 changed files with 41146 additions and 18707 deletions

3
.env.development Normal file
View File

@@ -0,0 +1,3 @@
# I tried and failed to set things here with vue-cli-service but
# things may be more reliable with vite so let's try again.

4
.env.production Normal file
View File

@@ -0,0 +1,4 @@
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app

View File

@@ -2,6 +2,7 @@ module.exports = {
root: true,
env: {
node: true,
es2022: true,
},
extends: [
"plugin:vue/vue3-essential",
@@ -9,9 +10,9 @@ module.exports = {
"@vue/typescript/recommended",
"plugin:prettier/recommended",
],
parserOptions: {
ecmaVersion: 2020,
},
// parserOptions: {
// ecmaVersion: 2020,
// },
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",

27
.github/workflows/playwright.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

7
.gitignore vendored
View File

@@ -2,8 +2,11 @@
node_modules
/dist
signature.bin
# generated during `npm run build`
sw_scripts-combined.js
*.pem
verified.txt
myenv
*~
# local env files
@@ -24,3 +27,7 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

311
CHANGELOG.md Normal file
View File

@@ -0,0 +1,311 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## ?
### Added
- Send list of contacts to someone
### Changed
- Moved contact actions from list onto detail page
## [0.3.20] - 2024.08.18 - 4064eb75a9743ca268bf00016fa0a5fc5dec4e30
### Fixed
- Bad "give" verbiage on offer page
- Failing offer test
## [0.3.19] - 2024.08.18 - ee9c14942ceba993bf21a11249601f205158ec71
### Added
- Update of an offer
- Recipient description in offer list
### Fixed
- List of offers wasn't showing.
- Destination page after sharing photo was wrong.
## [0.3.17] - 2024.07.11 - cefa384ff1a2d922848c370640c096c529920fab
### Added
- Photos on more screens
### Fixed
- Share of a photo, including sharing a photo from webkit/Safari which never worked
### Changed in DB or environment
- Nothing (though there's a new temp field in IndexedDB)
## [0.3.15] - 2024.08.04 - c8f0f2c2b16b9f0b4b47d40f7bf29058c7baa68e
### Added
- Edit gives
- Page to edit claim JSON before submitting
- Update of imported contacts
- Improve messaging on give dialog
- Section for gives provided by plan
- Deletion of an identity
- UI for choosing a passkey creation (not enabled on prod)
- Cache signatures for reports for passkey-signed requests
- Refactor: consolidate alternative signing, eg. for passkeys & did:peer
- Playwright tests
### Changed
- Linked projects display below description (instead of at bottom)
### Fixed
- Visibility toggle appearance
### Changed in DB or environment
- Nothing
## [0.3.14] - 2024.06.22 - 1611d22892f683f43856d2503eee7f391b6bbce8
### Added
- Clearer give-confirmation screen
- BX currency https://thebx.medium.com/
- Deselection of project on gifted details page
### Fixed
- Don't show registration pop-up for a new contact that is registered
### Changed in DB or environment
- Nothing
## [0.3.13] - 2024.05.24 - 08b67984e443c58d9178ad3776013b0bce7afddc
### Added
- Photos on projects
### Changed in DB or environment
- Nothing
## [0.3.12] - 2024.05.19 - 141fb39ad19c44d82fe1a33bf85115beacf50870
### Fixed
- Photo share (share_target) failed because requests were sent to server
### Changed in DB or environment
- Nothing
## [0.3.11] - 2024.05.19 - 567bcad88dfb7e9ac8fea72530d1163985e4a7cc
### Added
- Choose a file for gifts, and a URL for gifts & profiles
### Fixed
- Multiple button pushes were required to switch camera
### Changed in DB or environment
- Nothing
## [0.3.10] - 2024.05.11 - 03ac31d98110f7828cf9acb366db8d01b185f64c
### Added
- Share an image
- Choose a file on the device for a profile image
### Changed in DB or environment
- Nothing
## [0.3.9] - 2024.04.28 - 874e717e698b93a1ace9f588e675b8a3dccd7617
### Added
- Offers on contacts page
- Checks on front page until they show as registered
### Changed
- Scanned contacts now add immediately and prompt for registration.
- Better UI for gives on contact page
- Better UI for all confirmation messages
### Fixed
- Repeated elements at top of main feed
### Changed in DB or environment
- Nothing
## [0.3.8] - 2024.04.20 - 15c026c80ce03a26cae3ff80b0888934c101c7e2
### Added
- Profile image for user
### Fixed
- Slow loading of home page feed
### Changed in DB or environment
- Nothing
## [0.3.7] - 2024.04.10 - cf18f1543a700d62a5f9e764905a4aafe1fb229b
### Added
- Filter on home page feed
- Ability to set time of daily notification
- Jump to app on click of notification
### Changed
- Built with vite
- Descriptions on home page to include projects
### Changed in DB or environment
- Nothing
## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141
### Added
- Button to mirror photo during video
- More detailed onboarding help screen
- Public-data blurb
### Changed in DB or environment
- Nothing
## [0.3.5] - 2024.03.23 - 28754bdfb1e11aa221dd49a5dce4219b69cf6a9d
### Added
- Photo on gift records
### Fixed
- Environment variable for BVC meetings project
- Environment variables and build enhancements for test vs prod
### Changed in DB or environment
- New environment variable for image API server
- Test that a new browser session will get the right default APIs.
- Test that a new browser session will send the right BVC meetings project.
## [0.2.17] - 2024.03.01 - 3612ea42240c5e1b7d7eff29a39ff18f1b869b36
### Added
- Shortcut page for Bountiful Voluntaryist Community
### Changed
- More readable, targeted summaries in home-page feed items
### Changed in DB
- Nothing
## [0.2.14] - 2024.02.14 - 5f9edea1167dbfb64e16648764eed8c09b24eaeb
### Changed
- Combine all service worker scripts into a single file.
### Changed in DB
- Nothing
## [0.2.13] - 2024.02.07
### Added
- Display of user's offers
- Check for valid DIDs
### Fixed
- Name display on give prompt
- Non-numbers on number input & autocapitalize on URL input
### Changed in DB
- Nothing
## [0.2.12] - 2024.02.01
### Added
- Prompts for gratitude
## [0.2.11] - 2024.01.28
### Added
- Actions to share claim data with contacts
- Bulk CSV import from Endorser Mobile export
- Dates on give summaries
## [0.2.10] - 2024.01.18 - 667e1e8890b42de59cd939caca1a01c7a7a702be
### Added
- Person identicons for contacts
- Confirmation & delivery directly from project page
- Offer dialog now allows units
- Links from claim detail page to the fulfilled project or offer
- Link to project from home feed
- Copy to clipboard in more places
### Fixed
- "More Contacts" for give on project page now links correctly.
## [0.2.9] - 2024.01.15 - e5e702f8a5a53a6efbed48d35f0bc3cee63024a0
### Fixed
- Set visibility for new contact.
## [0.2.8] - 2024.01.14
### Added
- Automatic ID creation from home page
- Agent who can also edit a project
### Fixed
- Cannot declare anonymous gift
## [0.2.7] - 2024.01.12
### Added
- Give to fulfill a particular offer
- Give as part of a trade as opposed to a donation
- Error notifications on import
### Changed
- Library security updates
- Visibility of actions & confirmations on claim page
### Fixed
- Name of offerer
## [0.2.2] - 2024.01.05
### Added
- Check for notification capability on front screen
- Contact next-public-key-hash in manual textual input
- Confirmation for contact visibility change
- YAML rendering of full claim details
- Hints for onboarding on the contact screen
## [0.2.0] - 2024.01.04
### Added
- Contact next-public-key-hash
- Icon for Android
- More thorough messaging and testing for notifications
## [0.1.9] - 2024.01.01
### Added
- Import for contacts and settings
- Second download button for DuckDuckGo
### Changed
- Removed some keys from Dexie's IndexedDB declarations
## [0.1.8] - 2023.12.27- d26d1d360152a7d0e559b68486e85b72b88bd9ff
### Added
- 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
- Don't show actions on front page if not registered.
### Removed
- Home page Notiwind test buttons
## [0.1.2] - 2023.11.01 - 7f6c93802911a030a89fe3706e18b5c17151e5bb
### Added
- Basics: create ID, record a give, declare a project, search, and get notifications.

11
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,11 @@
# Contributing
Welcome! We are happy to have your help with this project.
We expect contributions to include automated tests and pass linting. Run the `test-all` task.
Note that some previous features don't have tests and adding more will make you friends quick.
Note that all contributions will be under our [license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE).
If you want to see a code of conduct, we're probably not the people you want to hang with.
Basically, we'll work together as long as we both enjoy it, and we'll stop when that stops.

329
README.md
View File

@@ -1,212 +1,223 @@
# kickstart-for-time-pwa
# TimeSafari.app - Crowd-Funder for Time - PWA
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
and expand to crowd-fund with time & money, then record and see the impact of contributions.
## Roadmap
See [project.task.yaml](project.task.yaml) for current priorities.
(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
## Setup
We like pkgx: `sh <(curl https://pkgx.sh) +npm sh`
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
### Compile and hot-reloads for development
```
npm run dev
```
### Build the test & production app
```
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
### Lint and fix files
```
npm run lint
```
### Test key contents
### Compile and minify for test & production
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
* `npx prettier --write ./sw_scripts/`
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
* Commit everything (since the commit hash is used the app).
* Record what version is currently on production.
* Run the correct build:
* Staging
```
# (Let's replace this with a .env.development or .env.staging file.)
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_PASSKEYS_ENABLED=yep npm run build
```
* Production
```
# This picks up values from .env.production
npm run build
```
* Get on the server and back up the time-safari/dist folder.
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
* Commit changes. Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
* [Tag with the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
## Tests
### Automated
Use the locally running Endorser server:
* Clone and set up https://github.com/trentlarson/endorser-ch and run the following in that directory:
```
test/test.sh
NODE_ENV=test-local npm run dev
```
* Now run the local tests:
```
npm run test-all
```
Note that a test will sometimes fail and rerunning may succeed (and repeat if a different test fails).
It's possible to use the global test Endorser (ledger) server (but currently the tests don't all succeed):
`npx playwright test`
It's possible to run with a minimal set of data: the following starts with the bare minimum of test data (but currently the tests don't all succeed):
```
rm ../endorser-ch-test-local.sqlite3
NODE_ENV=test-local npm run flyway migrate
NODE_ENV=test-local npm run test test/controller0
NODE_ENV=test-local npm run dev
```
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:
- Import the keys for the test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` by importing 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`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
- Alternatively, register someone else under User #0 automatically:
* In the `src/views/AccountViewView.vue` file, uncomment the lines referring to "testServerRegisterUser".
* Visit the `/account` page.
playing by importing that user and registering others. Import the keys for the test User
`did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F` by importing this seed phrase:
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
### 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.
### Customize Vue configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
### 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.
## Scenarios
### Icons
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
- 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`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
### Manual walk-through test
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
- Backup seed & data & get a CSV dump from Endorser Mobile.
- If there were any DB changes, check that you're on the old version and reload the page and ensure you can still act and haven't lost data (ie. contacts, identities).
- Use a mobile user as well as a desktop user.
- Check that the version is updated.
- Clear the browser data & add identity & import Time Safari contacts and then CSV contacts.
- Make sure that it's using the test API (under Identity in 'Advanced').
- Clear the browser data again. (See "Reset" below.)
- Go to the account page before visiting the home page to see that there is no ID.
- On the home page:
- Check that it generated an ID.
- Check the feed without names.
- Copy the contact URL.
- On each page, verify the messaging, and that they cannot take action.
- On the discovery page, check that they can see projects, and set a search area to see projects nearby.
- On the contacts page, check that they can add a contact even without their own ID.
- Install the PWA.
- As User 0 in another browser on the test API, add a give & a project.
- Note that some combinations of desktop with mobile emulation stretch the image.
- Import User 0 with seed: `rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
- Add new user as a contact (which allows them to see User 0).
- 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, import contacts & identifiers.
- 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.
- Switch back to the generated identifier.
- On the account page, check that they see messages on limits.
- As User 0, register the ID.
- As the new user on the home page, check that they can now record a gift, and record an offer & delivery.
- On the contacts page, check that they cannot register someone else yet.
- Walk through the functions on each page.
- Set and run notifications.
- Export & import, both seed and contacts & settings.
- Choose location on the search map.
- Offer, deliver a give, and confirm. Create a third user and test connections.
- On mobile, share an image with the app.
- Switch to "no identifier" to see that things look OK without any ID.
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
### Clear/Reset data & restart
### Clear 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 manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
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).
(If you find more, add them to the HelpNotificationsView.vue file.)
## Dependencies
## Troubleshooting
* A problem with `GET http://localhost:8080/web-push/vapid` means the py-push-server is not running
(and notifications won't work for a local app without special routing from the browser's web push service provider, anyway).
* Red errors everywhere with a console message like this:
`Error: An ID is chosen but there are no keys for it so it cannot be used to talk with the service`
... has happened on account switching when the current account was erased (or maybe replaced -- once I had a duplicate and I don't know how).
* The error `DEXIE ENCRYPT ADDON: Could not decrypt message!` or
`Encryption key has changed` means that the encryption key is wrong,
sometimes seen after clearing storage for testing; you can make it happen by clearing localStorage.
Maybe only part of the storage was cleared out. Unless you got a copy of that password, you'll
have to erase storage and reload the identifier.
See https://tea.xyz
| Project | Version |
| ---------- | --------- |
| nodejs.org | ^16.0.0 |
| npmjs.com | ^8.0.0 |
## Other
### Reference Material
```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
* 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.
// Import an existing ID
export const importAndStoreIdentifier = async (mnemonic: string, mnemonicPassword: string, toLowercase: boolean, previousIdentifiers: Array<IIdentifier>) => {
* [Customize Vue configuration](https://cli.vuejs.org/config/).
// just to get rid of variability that might cause an error
mnemonic = mnemonic.trim().toLowerCase()
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
/**
// 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!
* [WebStorm by JetBrains](https://www.jetbrains.com/webstorm/) for the free open-source license
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Many tools & libraries](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/src/branch/master/package.json#L10) such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [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)
* [DiceBear](https://www.dicebear.com/licenses/) and [Avataaars](https://www.dicebear.com/styles/avataaars/#details) for human-looking identicons
* Some gratitude prompts thanks to [Develop Good Habits](https://www.developgoodhabits.com/gratitude-journal-prompts/)

View File

@@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

76
doc/README.md Normal file
View File

@@ -0,0 +1,76 @@
# TimeSafari Docs
## Generating PDF from Markdown on OSx
This uses Pandoc and BasicTex (LaTeX) Installed through Homebrew.
### Set Up
```bash
brew install pandoc
brew install basictex
# Setting up LaTex packages
# First update tlmgr
sudo tlmgr update --self
# Then install LaTex packages
sudo tlmgr install bbding
sudo tlmgr install enumitem
sudo tlmgr install environ
sudo tlmgr install fancyhdr
sudo tlmgr install framed
sudo tlmgr install import
sudo tlmgr install lastpage # Enables Page X of Y
sudo tlmgr install mdframed
sudo tlmgr install multirow
sudo tlmgr install needspace
sudo tlmgr install ntheorem
sudo tlmgr install tabu
sudo tlmgr install tcolorbox
sudo tlmgr install textpos
sudo tlmgr install titlesec
sudo tlmgr install titling # Required for the fancy headers used
sudo tlmgr install threeparttable
sudo tlmgr install trimspaces
sudo tlmgr install tocloft # Required for \tableofcontents generation
sudo tlmgr install varwidth
sudo tlmgr install wrapfig
# Install fonts
sudo tlmgr install cmbright
sudo tlmgr install collection-fontsrecommended # And set up fonts
sudo tlmgr install fira
sudo tlmgr install fontaxes
sudo tlmgr install libertine # The main font the doc uses
sudo tlmgr install opensans
sudo tlmgr install sourceserifpro
```
#### References
The following guide was adapted to this project except that we install with Brew and have a few more packages.
Guide: https://daniel.feldroy.com/posts/setting-up-latex-on-mac-os-x
### Usage
Use the `pandoc` command to generate a PDF.
```bash
pandoc usage-guide.md -o usage-guide.pdf
```
And you can open the PDF with the `open` command.
```bash
open usage-guide.pdf
```
Or use this one-liner
```bash
pandoc usage-guide.md -o usage-guide.pdf && open usage-guide.pdf
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

316
doc/usage-guide.md Normal file
View File

@@ -0,0 +1,316 @@
---
geometry: margin=1in
header-includes:
- \usepackage{graphicx}
- \usepackage{titling}
- \usepackage{fancyhdr}
- \usepackage{lastpage}
- \pagestyle{fancy}
- \fancyhead[L]{Time Safari Usage Guide}
- \fancyhead[C]{Page \thepage\ of \pageref{LastPage}}
- \fancyhead[R]{}
- \fancyfoot[L]{}
- \fancyfoot[C]{}
- \fancyfoot[R]{\includegraphics[width=1cm]{images/timesafari-logo-binoculars.png}}
- \usepackage{tocloft}
- \usepackage{libertine}
- \renewcommand{\familydefault}{\sfdefault}
- \fancypagestyle{tocstyle}{
\fancyhead[L]{Time Safari Usage Guide}
\fancyhead[C]{Page \thepage\ of \pageref{LastPage}}
\fancyhead[R]{}
\fancyfoot[L]{}
\fancyfoot[C]{}
\fancyfoot[R]{\includegraphics[width=1cm]{images/timesafari-logo-binoculars.png}}}
---
\begin{titlepage}
\centering
\vspace*{\fill}
{\huge\textbf{TimeSafari Usage guide}}
\vspace{1cm}
{\Large Signing up users, adding contacts, and adding gifts.}
\vspace{1cm}
\includegraphics[width=0.5\textwidth]{images/timesafari-logo.png}
\vspace*{\fill}
\vspace{1cm}
{\Large Trent Larson, Kent Bull}
\vspace{0.5cm}
{\large 2024-06-25}
\end{titlepage}
\clearpage
\begin{center}
\includegraphics[width=2cm]{images/timesafari-logo-binoculars.png}
\end{center}
\tableofcontents
\clearpage
# Purpose of Document
Both end-users and development team members need to know how to use TimeSafari.
This document serves to show how to use every feature of the TimeSafari platform.
Sections of this document are geared specifically for software developers and quality assurance
team members.
Companion videos will also describe end-to-end workflows for the end-user.
# TimeSafari
## Overview
\pagebreak
# 1 - End Users
This section covers application usage for people who will use TimeSafari as intended. It is a
simplified guide illustrating how to gain value from using TimeSafari.
\pagebreak
# 2 - Software Developers
This section is tailored for software developers seeking to use the application during development,
quality assurance, and testing.
# Bootstrapping a local development environment
The first concern a software developer has when working on TimeSafari is to set up a local
development environment. This section will guide you through the process.
## Prerequisites
1. Have the following installed on your local machine:
- Node.js and NPM
- A web browser. For this guide, we will use Google Chrome.
- Git
- A code editor
2. Create an API key on Infura. This is necessary for the Endorser API to connect to the Ethereum
blockchain.
- You can create an account on Infura [here](https://infura.io/).\
Click "CREATE NEW API KEY" and label the key. Then click "API Keys" in the top menu bar to
be taken back to the list of keys.
Click "VIEW STATS" on the key you want to use.
![](images/01_infura-api-keys.png){ width=550px }
- Go to the key detail page. Then click "MANAGE API KEY".
![](images/02-infura-key-detail.png){ width=550px }
- Click the copy and paste button next to the string of alphanumeric characters.\
This is your API, also known as your project ID.
![](images/03-infura-api-key-id.png){width=550px }
- Save this for later during the Endorser API setup. This will go in your `INFURA_PROJECT_ID`
environment variable.
## Setup steps
### 1. Clone the following repositories from their respective Git hosts:
- [TimeSafari Frontend](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa)\
This is a Progressive Web App (PWA) built with VueJS and TypeScript.
Note that the clone command here is different from the one you would use for GitHub.
```bash
git clone git clone \
ssh://git@gitea.anomalistdesign.com:222/trent_larson/crowd-funder-for-time-pwa.git
```
- [TimeSafari Backend - Endorser API](https://github.com/trentlarson/endorser-ch)\
This is a NodeJS service providing the backend for TimeSafari.
```bash
git clone git@github.com:trentlarson/endorser-ch.git
```
\pagebreak
### 2. Database creation
#### Alternative 1 - use test data
To generate a development database and perform user setup you can run a local test with instructions
below to generate sample data. Then copy the test database, rename it to `-dev` as below:\
`cp ../endorser-ch-test-local.sqlite3 ../endorser-ch-dev.sqlite3` \
and rerun `npm run dev` to give yourself user #0 and others from the ETHR_CRED_DATA in [the endorser.ch test util file](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js#L90)
#### Alternative 2 - boostrap single seed user
In this method you will end up with two accounts in the database, one for the first boostrap user,
and the second as the primary user you will use during testing. The first user will invite the
second user to the app.
1. Install dependencies and environment variables.\
In endorser-ch install dependencies and set up environment variables to allow starting it up in
development mode.
```bash
cd endorser-ch
npm clean install # or npm ci
cp .env.local .env
```
Edit the .env file's INFURA_PROJECT_ID with the value you saved earlier in the
prerequisites.\
Then create the SQLite database by running `npm run flyway migrate` with environment variables
set correctly to select the default SQLite development user as follows.
```bash
export NODE_ENV=dev
export DBUSER=sa
export DBPASS=sasa
npm run flyway migrate
```
The first run of flyway migrate may take some time to complete because the entire Flyway
distribution must be downloaded prior to executing migrations.
Successful output looks similar to the following:
```
Database: jdbc:sqlite:../endorser-ch-dev.sqlite3 (SQLite 3.41)
Schema history table "main"."flyway_schema_history" does not exist yet
Successfully validated 10 migrations (execution time 00:00.034s)
Creating Schema History table "main"."flyway_schema_history" ...
Current version of schema "main": << Empty Schema >>
Migrating schema "main" to version "1 - initial-anew"
Migrating schema "main" to version "2 - registration"
Migrating schema "main" to version "3 - plan project"
Migrating schema "main" to version "4 - offer gave"
Migrating schema "main" to version "5 - more confirmations"
Migrating schema "main" to version "6 - providers urls"
Migrating schema "main" to version "7 - hash nonce"
Migrating schema "main" to version "8 - project location"
Migrating schema "main" to version "9 - plan links"
Migrating schema "main" to version "10 - gift or trade"
Successfully applied 10 migrations to schema "main", now at version v10 (execution time 00:00.043s)
A Flyway report has been generated here: /Users/kbull/code/timesafari/endorser-ch/report.html
```
\pagebreak
2. Generate the first user in TimeSafari PWA and bootstrap that user in Endorser's database.\
As TimeSafari is an invite-only platform the first user must be manually bootstrapped since
no other users exist to be able to invite the first user. This first user must be added manually
to the SQLite database used by Endorser. In this setup you generate the first user from the PWA.
This user is automatically generated on first usage of the TimeSafari PWA. Bootstrapping that
user is required so that this first user can register other users.
- Change directories into `crowd-funder-for-time-pwa`
```bash
cd ..
cd crowd-funder-for-time-pwa
```
- Ensure the `.env.development` file exists and has the following values:
```env
VITE_DEFAULT_ENDORSER_API_SERVER=http://127.0.0.1:3000
```
- Install dependencies and run in dev mode. For now don't worry about configuring the app. All we
need is to generate the first root user and this happens automatically on app startup.
```bash
npm clean install # or npm ci
npm run dev
```
- Open the app in a browser and go to the developer tools. It is recommended to use a completely
separate browser profile so you do not clear out your existing user account. We will be
completely resetting the PWA app state prior to generating the first user.
In the Developer Tools go to the Application tab.
![](images/04-pwa-chrome-devtools.png){width=350px}
Click the "Clear site data" button and then refresh the page.
- Click the account button in the bottom right corner of the page.
![](images/05-pwa-account-button.png){width=150px}
- This will take you to the account page titled "Your Identity" on which you can see your DID,
a `did:ethr` DID in this case.
![](images/06-pwa-account-page.png){width=350px}
- Copy the DID by selecting it and copying it to the clipboard or by clicking the copy and paste
button as shown in the image.
![](images/07-pwa-did-copied.png){width=200px}
In our case this DID is:\
`did:ethr:0xe4B783c74c8B0e229524e44d0cD898D272E02CD6`
- Add that DID to the following echoed SQL statement where it says `YOUR_DID`
```bash
echo "INSERT INTO registration (did, maxClaims, maxRegs, epoch)
VALUES ('YOUR_DID', 100, 10000, 1719348718092);"
| sqlite3 ./endorser-ch-dev.sqlite3
```
and run this command in the parent directory just above the `endorser-ch` directory.
It needs to be the parent directory of your `endorser-ch` repository because when
`endorser-ch` creates the SQLite database it depends on it creates it in the parent directory
of `endorser-ch`.
- You can verify with an SQL browser tool that your record has been added to the `registration`
table.
![](images/08-endorser-sqlite-row-added.png){width=350px}
3. Then start the Endorser service in development mode with the following commands.
```bash
cd ./endorser-ch
export NODE_ENV=dev
npm run dev
```
This starts the Endorser service on port 3000.
4. Create the second user by opening up a separate browser profile or incognito session, opening the
TimeSafari PWA at `http://localhost:8080`. You will see the yellow banner stating "Someone must
register you before you can give or offer."
![](images/09-pwa-second-profile-first-open.png){width=350px}
- If you want to ensure you have a fresh user account then open the developer tools, clear the
Application data as before, and then refresh the page. This will generate a new user in the
browser's IndexedDB database.
5. Go to the second users' account page to copy the DID.
![](images/10-pwa-second-user-did.png){width=350px}
6. Copy the DID and put it in the text bar on the "Your Contacts" page for the first account
![](images/11-pwa-first-user-add-contact.png){width=350px}
7. Click the "+" plus icon to add the user.
![](images/12-pwa-first-user-contact-added.png){width=350px}
8. Then click the register button to register the second user.
![](images/13-pwa-first-user-register-second-user-btn.png){width=350px}
9. Click "YES" on the dialog that shows up.
![](images/14-pwa-first-user-register-yes.png){width=350px}
After this a notification will pop up indicating whether registration was successful or not.
10. You have finished the initial set up of users.

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>TimeSafari</title>
</head>
<body>
<noscript>
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,8 +1,11 @@
Prerequisites:
JWT Creation & Verification
jq
To run this in a script, see ./openssl_signing_console.sh
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
Prerequisites: openssl, jq
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using
a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
@@ -15,20 +18,22 @@ openssl ec -in private.pem -pubout -out public.pem
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
Next, create a payload object as a JSON object containing the claims you want to include in the JWT. For example schema.org :
Next, create a payload object as a JSON object containing the claims you want to include in the JWT.
For example schema.org :
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
Concatenate the encoded header, payload, and a secret to create the signing input:
signing_input="$header_b64.$payload_b64"
Create the signature by signing the signing input with a ES256K algorithm and your secret. You can use the openssl command line utility to do this:
Create the signature by signing the signing input with a ES256K algorithm and your secret.
You can use the openssl command line utility to do this:
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
@@ -43,7 +48,7 @@ Authorization: Bearer $jwt
To verify the JWT, you can use the openssl utility with the public key:
openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
This will verify the signature and output Verified OK if the signature is valid. If the signature is not valid, it will output an error.
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
This will verify the signature and output "Verified OK" if the signature is valid.
If the signature is not valid, it will give an error response and output "Verification failure".

View File

@@ -1,25 +1,39 @@
#!/bin/bash
# Generate a JWT, with signature verified using OpenSSL
#
# Prerequisites: openssl, jq
#
# Usage: source ./openssl_signing_console.sh
#
# For a more complete explanation, see ./openssl_signing_console.rst
# 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')
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
signing_input="$header_b64.$payload_b64"
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem | openssl base64 -e)
# Read binary signature from file and encode it to Base64 URL-Safe format
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature" | openssl base64 -d)
# Read binary signature and encode it to Base64 URL-Safe format
signature_b64=$(echo -n "$signature" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
# Construct the JWT
jwt="$signing_input.$signature_b64"
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
echo Resulting JWT: $jwt

23213
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +1,101 @@
{
"name": "kickstart-for-time-pwa",
"version": "0.1.0",
"private": true,
"name": "TimeSafari",
"version": "0.3.21-beta",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"dev": "vite",
"serve": "vite preview",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build",
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
"test-local": "npx playwright test -c playwright.config-local.ts --trace on",
"test-all": "npm run build && npx playwright test -c playwright.config-local.ts --trace on"
},
"dependencies": {
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",
"@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0",
"@veramo/core": "^5.2.0",
"@veramo/credential-w3c": "^5.2.0",
"@veramo/data-store": "^5.2.0",
"@veramo/did-manager": "^5.1.2",
"@veramo/did-provider-ethr": "^5.1.2",
"@veramo/did-resolver": "^5.2.0",
"@veramo/key-manager": "^5.1.2",
"@vueuse/core": "^10.2.1",
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.0",
"@tweenjs/tween.js": "^21.1.1",
"@veramo/core": "^5.6.0",
"@veramo/credential-w3c": "^5.6.0",
"@veramo/data-store": "^5.6.0",
"@veramo/did-manager": "^5.6.0",
"@veramo/did-provider-ethr": "^5.6.0",
"@veramo/did-provider-peer": "^6.0.0",
"@veramo/did-resolver": "^5.6.0",
"@veramo/key-manager": "^5.6.0",
"@vueuse/core": "^10.9.0",
"@zxing/text-encoding": "^0.9.0",
"axios": "^1.4.0",
"buffer": "^6.0.3",
"asn1-ber": "^1.2.2",
"axios": "^1.6.8",
"cbor-x": "^1.5.9",
"class-transformer": "^0.5.1",
"core-js": "^3.31.1",
"dexie": "^3.2.4",
"dexie-export-import": "^4.0.7",
"did-jwt": "^7.2.4",
"ethereum-cryptography": "^2.0.0",
"dexie": "^3.2.7",
"dexie-export-import": "^4.1.1",
"did-jwt": "^7.4.7",
"ethereum-cryptography": "^2.1.3",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"localstorage-slim": "^2.4.0",
"luxon": "^3.3.0",
"merkletreejs": "^0.3.10",
"moment": "^2.29.4",
"js-yaml": "^4.1.0",
"localstorage-slim": "^2.7.0",
"lru-cache": "^10.2.0",
"luxon": "^3.4.4",
"merkletreejs": "^0.3.11",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0",
"pinia-plugin-persistedstate": "^3.2.1",
"qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.0",
"readable-stream": "^4.4.2",
"reflect-metadata": "^0.1.13",
"ramda": "^0.29.1",
"readable-stream": "^4.5.2",
"reflect-metadata": "^0.1.14",
"register-service-worker": "^1.7.2",
"three": "^0.154.0",
"vue": "^3.3.4",
"simple-vue-camera": "^1.1.3",
"three": "^0.156.1",
"ua-parser-js": "^1.0.37",
"util": "^0.12.5",
"vue": "^3.4.21",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^2.1.20",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.2.3",
"vue-facing-decorator": "^3.0.4",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.3.0",
"web-did-resolver": "^2.0.27"
},
"devDependencies": {
"@types/ramda": "^0.29.3",
"@types/three": "^0.152.1",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"@playwright/test": "^1.45.2",
"@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2",
"@types/node": "^20.14.11",
"@types/ramda": "^0.29.11",
"@types/three": "^0.155.1",
"@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^5.0.0-alpha.1",
"eslint-plugin-vue": "^9.15.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.23.0",
"leaflet": "^1.9.4",
"postcss": "^8.4.24",
"prettier": "^3.0.0",
"tailwindcss": "^3.3.2",
"typescript": "~5.1.6"
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.1",
"typescript": "~5.2.2",
"vite": "^5.2.0",
"vite-plugin-pwa": "^0.19.8"
}
}

View File

@@ -0,0 +1,98 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './test-playwright',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:8080',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
permissions: ["clipboard-read"],
},
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
},
],
/* Configure global timeout; default is 30000 milliseconds */
// the image upload will often not succeed at 5 seconds
timeout: 20000,
/* Run your local dev server before starting the tests */
/**
* This could be an array of servers, meaning we could start the Endorser server as well:
* {
* command: "cd ../endorser-ch; NODE_ENV=test-local npm run dev",
* url: 'http://localhost:3000',
* reuseExistingServer: !process.env.CI,
* },
*
* But if we do then the testInfo.config.webServer is null and the API-setting test 00 fails.
* It is worth considering a change such that Time Safari's default Endorser API server is NOT set
* in the user's settings so that it can be blanked out and the default is used.
*/
webServer: {
command:
"VITE_PASSKEYS_ENABLED=true VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 npm run dev",
url: "http://localhost:8080",
reuseExistingServer: !process.env.CI,
},
});

82
playwright.config.ts Normal file
View File

@@ -0,0 +1,82 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './test-playwright',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'https://test.timesafari.app',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
permissions: ["clipboard-read"],
},
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command:
// "VITE_PASSKEYS_ENABLED=true VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 npm run dev",
// url: "http://localhost:8080",
// reuseExistingServer: !process.env.CI,
// },
});

View File

@@ -1,112 +1,4 @@
tasks:
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
- 01 add a location for a project via map pin :
- add with a "location" field containing this: { "geo":{ "@type":"GeoCoordinates", "latitude":40.883944, "longitude":-111.884787 } }
- 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
tasks :
- 01 add a location for a project via map pin
- 04 search by a bounding box for local projects (see API by clicking on "Nearby")
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
- 02 Fix images on projectview - allow choice of image from a pallete of images or a url image (discovery page display also)
- SEE: https://github.com/dmester/jdenticon assignee:jose
- 08 Scan QR code to import into contacts assignee:matthew
- SEE: https://github.com/gruhn/vue-qrcode-reader
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s) assignee-group:ui
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui
- 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
- 24 Move to Vite assignee:matthew
- .5 include the hash of the latest commit, and maybe a version
- .5 add link to further project / people when a project pays ahead
- .5 add project ID to the URL, to make a project publicly-accessible
- .5 remove edit from project page for projects owned by others
- .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 assignee-group:ui
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
- .2 move 'switch identity' to the advanced section assignee-group:ui
- .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"
- 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
- .5 change the derivation path, and regenerate test IDs
- 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 assignee-group:ui
- .5 customize favicon assignee-group:ui
- .5 Do we want to combine first name & last name?
- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
- 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
- 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)
- Release Minimum Viable Product :
- 08 thorough testing for errors & edge cases
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- linking between projects or plans :
- show total time given to & from a project
- terminology:
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
- Stats :
- 01 point out user's location on the world
- 01 present a credential selected from the stats
- 04 show gives spreading to other places
- badge for most gives/receives/confirms per day/week/month
- badge for amount given/offered to your project
- set a goal of given/offers
- automated tests, eg. cypress
- Notifications (wake on the phone, push notifications)
- Connect with phone contacts
- Multiple identities
- Peer DID
- DIDComm
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
- Do we want split first name & last name?
- 40 notifications v+ :
- pull, w/ scheduled runs
log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27
- This list has moved - see https://sharing.clickup.com/9014278710/l/h/6-901402735154-1/685f1be3f9b150d

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

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 150 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,17 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,7 +1,7 @@
<template>
<router-view />
<!-- https://github.com/emmanuelsw/notiwind -->
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
<NotificationGroup group="alert">
<div
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
@@ -129,6 +129,10 @@
</div>
</NotificationGroup>
<!--
This "group" of "modal" is the prompt for an answer.
Set "type" as follows: "confirm" for yes/no, and "notification" ones: "-permission", "-mute", "-off"
-->
<NotificationGroup group="modal">
<div class="fixed z-[100] top-0 inset-x-0 w-full">
<Notification
@@ -142,12 +146,97 @@
move="transition duration-500"
move-delay="delay-300"
>
<!-- see NotificationIface in constants/app.ts -->
<div
v-for="notification in notifications"
:key="notification.id"
class="w-full"
role="alert"
>
<!--
Type of "confirm" will post a message.
With onYes function, show a "Yes" button to call that function.
With onNo function, show a "No" button to call that function,
and pass it state of "askAgain" field shown if you set promptToStopAsking.
-->
<div
v-if="notification.type === 'confirm'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<span class="font-semibold text-lg">
{{ notification.title }}
</span>
<p class="text-sm mb-2">{{ notification.text }}</p>
<button
v-if="notification.onYes"
@click="
notification.onYes();
close(notification.id);
"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Yes
{{ notification.yesText ? ", " + notification.yesText : "" }}
</button>
<button
v-if="notification.onNo"
@click="
notification.onNo(stopAsking);
close(notification.id);
stopAsking = false; // reset value
"
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
>
No {{ notification.noText ? ", " + notification.noText : "" }}
</button>
<label
v-if="notification.promptToStopAsking && notification.onNo"
for="toggleStopAsking"
class="flex items-center justify-between cursor-pointer my-4"
@click="stopAsking = !stopAsking"
>
<!-- label -->
<span class="ml-2">... and do not ask again.</span>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="stopAsking"
name="stopAsking"
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>
<button
@click="
notification.onCancel
? notification.onCancel(stopAsking)
: null;
close(notification.id);
stopAsking = false; // reset value
"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
{{ notification.onYes ? "Cancel" : "Close" }}
</button>
</div>
</div>
</div>
<div
v-if="notification.type === 'notification-permission'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
@@ -156,28 +245,121 @@
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<p class="text-lg mb-4">
Would you like to turn on notifications for this app?
<p v-if="serviceWorkerReady" class="text-lg mb-4">
Would you like to be notified of new activity once a day?
</p>
<p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 10
seconds...
<fa icon="spinner" spin />
</p>
<div v-if="serviceWorkerReady">
<span class="flex flex-row justify-center">
<span class="mt-2">Yes, tell me at: </span>
<input
type="number"
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
v-model="hourInput"
/>
<span
class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
@click="hourAm = !hourAm"
>
<span v-if="hourAm"> AM <fa icon="chevron-down" /> </span>
<span v-else> PM <fa icon="chevron-up" /> </span>
</span>
</span>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md"
@click="
() => {
if (checkHour()) {
close(notification.id);
turnOnNotifications();
}
}
"
>
Turn on Daily Message
</button>
</div>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
>
No, Not Now
</button>
</div>
</div>
</div>
<div
v-if="notification.type === 'notification-mute'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<p class="text-lg mb-4">Mute app notifications:</p>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Turn on Notifications
For 1 Hour
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
For 8 Hours
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
For 24 Hours
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Until I turn it back on
</button>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Cancel
</button>
</div>
</div>
</div>
<div
v-if="notification.type === 'notification-off'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<p class="text-lg mb-4">
Would you like to <b>turn off</b> notifications for this app?
</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
</button>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Leave it On
</button>
<div class="grid grid-cols-2 gap-2">
<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>
<button
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
>
Never
</button>
</div>
</div>
</div>
</div>
@@ -189,4 +371,421 @@
<style></style>
<script lang="ts"></script>
<script lang="ts">
import axios from "axios";
import { Vue, Component } from "vue-facing-decorator";
import * as libsUtil from "@/libs/util";
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;
};
}
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
notifyTime: { utcHour: number };
}
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { sendTestThroughPushServer } from "@/libs/util";
@Component
export default class App extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
stopAsking = false;
b64 = "";
hourAm = true;
hourInput = "8";
serviceWorkerReady = true;
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;
}
if (pushUrl.startsWith("http://localhost")) {
console.log("Not checking for VAPID in this local environment.");
} else {
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,
);
}
}
// there may be a long pause here on first initialization
navigator.serviceWorker?.ready.then(() => {
this.serviceWorkerReady = true;
});
}
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> {
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.");
}
if (Notification.permission === "granted") {
return Promise.resolve();
}
return Promise.resolve();
}
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.");
}
return permission;
});
}
// this allows us to show an error without closing the dialog
checkHour() {
if (!libsUtil.isNumeric(this.hourInput)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Number",
text: "The time must be an hour number.",
},
5000,
);
return false;
}
const hourNum = libsUtil.numberOrZero(this.hourInput);
if (!Number.isInteger(hourNum)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Whole Number",
text: "The time must be a whole hour number.",
},
5000,
);
return false;
}
if (hourNum < 1 || 12 < hourNum) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Whole Number",
text: "The time must be an hour between 1 and 12.",
},
5000,
);
return false;
}
return true;
}
public async turnOnNotifications() {
return this.askPermission()
.then((permission) => {
console.log("Permission granted:", permission);
// Call the function and handle promises
this.subscribeToPush()
.then(() => {
console.log("Subscribed successfully.");
return navigator.serviceWorker?.ready;
})
.then((registration) => {
return registration.pushManager.getSubscription();
})
.then(async (subscription) => {
if (subscription) {
await this.$notify(
{
group: "alert",
type: "info",
title: "Notification Setup Underway",
text: "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page.",
},
-1,
);
// we already checked that this is a valid hour number
const rawHourNum = libsUtil.numberOrZero(this.hourInput);
const adjHourNum = rawHourNum + (this.hourAm ? 0 : 12);
const hourNum = adjHourNum % 24;
const utcHour =
hourNum + Math.round(new Date().getTimezoneOffset() / 60);
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
const subscriptionWithTime: PushSubscriptionWithTime = {
notifyTime: { utcHour: finalUtcHour },
...subscription.toJSON(),
};
await this.sendSubscriptionToServer(subscriptionWithTime);
return subscriptionWithTime;
} else {
throw new Error("Subscription object is not available.");
}
})
.then(async (subscription: PushSubscriptionWithTime) => {
console.log(
"Subscription data sent to server and all finished successfully.",
);
await sendTestThroughPushServer(subscription, true);
this.$notify(
{
group: "alert",
type: "success",
title: "Notifications Turned On",
text: "Notifications are on. You should see at least one on your device; if not, check 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 setting notification permissions:",
error,
);
alert("Some error occurred setting notification permissions.");
});
}
private urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/-/g, "+")
.replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
private subscribeToPush(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!("serviceWorker" in navigator && "PushManager" in window)) {
const errorMsg = "Push messaging is not supported";
console.warn(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: PushSubscriptionWithTime,
): Promise<void> {
console.log("About to send subscription...", subscription);
return fetch("/web-push/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subscription),
}).then((response) => {
if (!response.ok) {
throw new Error("Failed to send subscription to server");
}
console.log("Subscription sent to server successfully.");
});
}
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.error("Push provider server communication failed:", error);
return false;
});
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.error("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>

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect width="64" height="64" fill="#ffffff"></rect>
</svg>

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

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

After

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

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

@@ -1,9 +1,8 @@
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
@layer base {
html {
font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important;

View File

@@ -1,47 +0,0 @@
<template>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-amber-400 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
@Component
export default class AlertMessage extends Vue {
@Prop alertTitle = "";
@Prop alertMessage = "";
isAlertVisible = this.alertMessage;
public onClickClose() {
this.isAlertVisible = false;
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-amber-200": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

View File

@@ -1,18 +1,40 @@
<template>
<div v-html="generateIdenticon()" class="w-fit"></div>
<div v-html="generateIcon()" class="w-fit"></div>
</template>
<script lang="ts">
import { createAvatar, StyleOptions } from "@dicebear/core";
import { avataaars } from "@dicebear/collection";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { toSvg } from "jdenticon";
import { Contact } from "@/db/tables/contacts";
@Component
export default class EntityIcon extends Vue {
@Prop entityId = "";
@Prop iconSize = "";
@Prop contact: Contact;
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
@Prop iconSize = 0;
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
generateIdenticon() {
const svgString = toSvg(this.entityId, this.iconSize);
return svgString;
generateIcon() {
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
if (imageUrl) {
return `<img src="${imageUrl}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
} else {
const identifier = this.contact?.did || this.entityId;
if (!identifier) {
return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
}
// https://api.dicebear.com/8.x/avataaars/svg?seed=
// ... does not render things with the same seed as this library.
// "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b" yields a girl with flowers in her hair and a lightning earring
// ... which looks similar to '' at the dicebear site but which is different.
const options: StyleOptions<object> = {
seed: (identifier as string) || "",
size: this.iconSize,
};
const avatar = createAvatar(avataaars, options);
const svgString = avatar.toString();
return svgString;
}
}
}
</script>

View File

@@ -0,0 +1,219 @@
<template>
<div v-if="visible" id="dialogFeedFilters" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Feed Filters</h1>
<p class="mb-4 font-bold">Show only activities that</p>
<div class="grid grid-cols-1 gap-2">
<div
class="flex items-center justify-between cursor-pointer"
@click="toggleHasVisibleDid()"
>
<!-- label -->
<div>Include someone visible to me</div>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="hasVisibleDid"
name="toggleFilterFromMyContacts"
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>
</div>
<em>or</em>
<div
class="flex items-center justify-between cursor-pointer"
@click="
hasSearchBox
? toggleNearby()
: $router.push({ name: 'search-area' })
"
>
<!-- label -->
<div>Are nearby</div>
<!-- toggle -->
<div v-if="hasSearchBox" class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="isNearby"
name="toggleFilterNearby"
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>
<div v-else class="relative ml-2">
<button class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500">
Select Location
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-4">
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="setAll()"
>
Set All
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="clearAll()"
>
Clear All
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="done()"
>
Done
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import {
LMap,
LMarker,
LRectangle,
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { db } from "@/db/index";
@Component({
components: {
LRectangle,
LMap,
LMarker,
LTileLayer,
},
})
export default class FeedFilters extends Vue {
onCloseIfChanged = () => {};
hasSearchBox = false;
hasVisibleDid = false;
isNearby = false;
settingChanged = false;
visible = false;
async open(onCloseIfChanged: () => void) {
this.onCloseIfChanged = onCloseIfChanged;
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.hasVisibleDid = !!settings?.filterFeedByVisible;
this.isNearby = !!settings?.filterFeedByNearby;
if (settings?.searchBoxes && settings.searchBoxes.length > 0) {
this.hasSearchBox = true;
}
this.settingChanged = false;
this.visible = true;
}
async toggleHasVisibleDid() {
this.settingChanged = true;
this.hasVisibleDid = !this.hasVisibleDid;
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid,
});
}
async toggleNearby() {
this.settingChanged = true;
this.isNearby = !this.isNearby;
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: this.isNearby,
});
}
async clearAll() {
if (this.hasVisibleDid || this.isNearby) {
this.settingChanged = true;
}
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: false,
filterFeedByVisible: false,
});
this.hasVisibleDid = false;
this.isNearby = false;
}
async setAll() {
if (!this.hasVisibleDid || !this.isNearby) {
this.settingChanged = true;
}
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: true,
filterFeedByVisible: true,
});
this.hasVisibleDid = true;
this.isNearby = true;
}
close() {
if (this.settingChanged) {
this.onCloseIfChanged();
}
this.visible = false;
}
done() {
this.close();
}
}
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
#dialogFeedFilters.dialog-overlay {
z-index: 99999;
overflow: scroll;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
</style>

View File

@@ -2,29 +2,32 @@
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giver?.name || "somebody not specified" }}
{{ customTitle }}
</h1>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received"
placeholder="What was given"
v-model="description"
/>
<div class="flex flex-row mb-6">
<div class="flex flex-row justify-center">
<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
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()"
>
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()"
@click="amountInput === '0' ? null : 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"
id="inputGivenAmount"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@@ -33,74 +36,354 @@
<fa icon="chevron-right" />
</div>
</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 class="mt-4 flex justify-center">
<span>
<router-link
:to="{
name: 'gifted-details',
query: {
amountInput,
description,
giverDid: giver?.did,
giverName: giver?.name,
offerId,
projectId,
recipientDid: receiver?.did,
recipientName: receiver?.name,
unitCode,
},
}"
class="text-blue-500"
>
Photo & more options ...
</router-link>
</span>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel"
>
Cancel
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import {
createAndSubmitGive,
didInfo,
GiverReceiverInputInfo,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Contact } from "@/db/tables/contacts";
@Component
export default class GiftedDialog extends Vue {
@Prop message = "";
$notify!: (notification: NotificationIface, timeout?: number) => void;
giver = null;
@Prop projectId = "";
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
amountInput = "0";
callbackOnSuccess?: (amount: number) => void = () => {};
customTitle?: string;
description = "";
hours = "0";
giver?: GiverReceiverInputInfo; // undefined means no identified giver agent
isTrade = false;
offerId = "";
receiver?: GiverReceiverInputInfo;
unitCode = "HUR";
visible = false;
open(giver) {
// giver: GiverInputInfo
libsUtil = libsUtil;
async open(
giver?: GiverReceiverInputInfo,
receiver?: GiverReceiverInputInfo,
offerId?: string,
customTitle?: string,
callbackOnSuccess?: (amount: number) => void,
) {
this.customTitle = customTitle;
this.description = "";
this.giver = giver;
this.receiver = receiver;
// if we show "given to user" selection, default checkbox to true
this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || "";
try {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
if (this.giver && !this.giver.name) {
this.giver.name = didInfo(
this.giver.did,
this.activeDid,
this.allMyDids,
this.allContacts,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("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,
);
}
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.libsUtil.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,
)}`;
}
@Emit("dialog-result")
confirm() {
const result = {
action: "confirm",
giver: this.giver,
hours: parseFloat(this.hours),
description: this.description,
};
this.close();
this.description = "";
this.giver = null;
this.hours = "0";
return result;
}
@Emit("dialog-result")
cancel() {
const result = { action: "cancel" };
this.close();
return result;
this.eraseValues();
}
eraseValues() {
this.description = "";
this.giver = undefined;
this.amountInput = "0";
this.unitCode = "HUR";
}
async confirm() {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record a give.",
},
3000,
);
return;
}
if (parseFloat(this.amountInput) < 0) {
this.$notify(
{
group: "alert",
type: "danger",
text: "You may not send a negative number.",
title: "",
},
2000,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${
this.libsUtil.UNIT_LONG[this.unitCode]
}.`,
},
2000,
);
return;
}
this.close();
this.$notify(
{
group: "alert",
type: "toast",
text: "Recording the give...",
title: "",
},
1000,
);
// this is asynchronous, but we don't need to wait for it to complete
await this.recordGive(
(this.giver?.did as string) || null,
(this.receiver?.did as string) || null,
this.description,
parseFloat(this.amountInput),
this.unitCode,
).then(() => {
this.eraseValues();
});
}
/**
*
* @param giverDid may be null
* @param recipientDid may be null
* @param description may be an empty string
* @param amount may be 0
* @param unitCode may be omitted, defaults to "HUR"
*/
async recordGive(
giverDid: string | null,
recipientDid: string | null,
description: string,
amount: number,
unitCode: string = "HUR",
) {
try {
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
this.activeDid,
giverDid as string,
recipientDid as string,
description,
amount,
unitCode,
this.projectId,
this.offerId,
this.isTrade,
);
if (
result.type === "error" ||
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.error("Error with give creation result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error creating the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
},
7000,
);
if (this.callbackOnSuccess) {
this.callbackOnSuccess(amount);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error with give recordation caught:", error);
const errorMessage =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the give.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage,
},
-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
isGiveCreationError(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
getGiveCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
);
}
explainData() {
this.$notify(
{
group: "alert",
type: "success",
title: "Data Sharing",
text: libsUtil.PRIVACY_MESSAGE,
},
-1,
);
}
}
</script>

View File

@@ -0,0 +1,242 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4 relative">
Here's one:
<div
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
@click="cancel"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</h1>
<span class="flex justify-between">
<span
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
@click="prevIdea()"
>
<fa icon="chevron-left" class="m-auto" />
</span>
<div class="m-2">
<span v-if="currentIdeaIndex < IDEAS.length">
<p class="text-center text-lg font-bold">
{{ IDEAS[currentIdeaIndex] }}
</p>
</span>
<div v-if="currentIdeaIndex == IDEAS.length + 0">
<p class="text-center">
<span
v-if="currentContact == null"
class="text-orange-500 text-lg font-bold"
>
That's all your contacts.
</span>
<span v-else>
<span class="text-lg font-bold">
Did {{ currentContact.name || AppString.NO_CONTACT_NAME }}
<br />
or someone near them do anything &ndash; maybe a while ago?
</span>
<span class="flex justify-between">
<span />
<button
class="text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4"
@click="nextIdeaPastContacts()"
>
Skip Contacts <fa icon="forward" />
</button>
</span>
</span>
</p>
</div>
</div>
<span
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2 flex"
@click="nextIdea()"
>
<fa icon="chevron-right" class="m-auto" />
</span>
</span>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4"
@click="cancel"
>
That's it!
</button>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
@Component
export default class GivenPrompts extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
IDEAS = [
"Did anyone fix food for you?",
"Did a family member do something for you?",
"Did anyone give you a compliment?",
"Who is someone you can always rely on, and how did they demonstrate that?",
"Did you see anyone give to someone else?",
"Is there someone who you have never met who has helped you somehow?",
"How did an artist or musician or author inspire you?",
"What inspiration did you get from someone who handled tragedy well?",
"Did some organization give something worth respect?",
"Who last gave you a good laugh?",
"Do you recall anything that was given to you while you were young?",
"Did someone forgive you or overlook a mistake?",
"Do you know of a way an ancestor contributed to your life?",
"Did anyone give you help at work?",
"How did a teacher or mentor or great example help you?",
];
OTHER_PROMPTS = 1;
CONTACT_PROMPT_INDEX = this.IDEAS.length; // expected after other prompts
currentContact: Contact | undefined = undefined;
currentIdeaIndex = 0;
numContacts = 0;
shownContactDbIndices: number[] = [];
visible = false;
AppString = AppString;
async open() {
this.visible = true;
await db.open();
this.numContacts = await db.contacts.count();
}
close() {
// close the dialog but don't change values (just in case some actions are added later)
this.visible = false;
}
/**
* Get the next idea.
* If it is a contact prompt, loop through.
*/
async nextIdea() {
// if we're incrementing to the contact prompt
// or if we're at the contact prompt and there was a previous contact...
if (
this.currentIdeaIndex == this.CONTACT_PROMPT_INDEX - 1 ||
(this.currentIdeaIndex == this.CONTACT_PROMPT_INDEX &&
this.shownContactDbIndices.length < this.numContacts)
) {
this.currentIdeaIndex = this.CONTACT_PROMPT_INDEX;
this.findNextUnshownContact();
} else {
// we're not at the contact prompt (or we ran out), so increment the idea index
this.currentIdeaIndex =
(this.currentIdeaIndex + 1) % (this.IDEAS.length + this.OTHER_PROMPTS);
// ... and clear out any other prompt info
this.currentContact = undefined;
this.shownContactDbIndices = [];
}
}
prevIdea() {
if (
this.currentIdeaIndex ==
(this.CONTACT_PROMPT_INDEX + 1) %
(this.IDEAS.length + this.OTHER_PROMPTS) ||
(this.currentIdeaIndex == this.CONTACT_PROMPT_INDEX &&
this.shownContactDbIndices.length < this.numContacts)
) {
this.currentIdeaIndex = this.CONTACT_PROMPT_INDEX;
this.findNextUnshownContact();
} else {
// we're not at the contact prompt (or we ran out), so increment the idea index
this.currentIdeaIndex--;
if (this.currentIdeaIndex < 0) {
this.currentIdeaIndex = this.IDEAS.length - 1 + this.OTHER_PROMPTS;
}
// ... and clear out any other prompt info
this.currentContact = undefined;
this.shownContactDbIndices = [];
}
}
nextIdeaPastContacts() {
this.currentIdeaIndex = 0;
this.currentContact = undefined;
this.shownContactDbIndices = [];
}
async findNextUnshownContact() {
// get a random contact
if (this.shownContactDbIndices.length === this.numContacts) {
// no more contacts to show
this.currentContact = undefined;
} else {
// get a random contact that hasn't been shown yet
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
// and guarantee that one is found by walking past shown contacts
let shownContactIndex =
this.shownContactDbIndices.indexOf(someContactDbIndex);
while (shownContactIndex !== -1) {
// increment both indices until we find a spot where "shown" skips a spot
shownContactIndex = (shownContactIndex + 1) % this.numContacts;
someContactDbIndex = (someContactDbIndex + 1) % this.numContacts;
if (
this.shownContactDbIndices[shownContactIndex] !== someContactDbIndex
) {
// we found a contact that hasn't been shown yet
break;
}
// continue
// ... and there must be at least one because shownContactDbIndices length < numContacts
}
this.shownContactDbIndices.push(someContactDbIndex);
this.shownContactDbIndices.sort();
// get the contact at that offset
await db.open();
this.currentContact = await db.contacts
.offset(someContactDbIndex)
.first();
}
}
cancel() {
this.currentContact = undefined;
this.currentIdeaIndex = 0;
this.numContacts = 0;
this.shownContactDbIndices = [];
this.close();
}
}
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
</style>

View File

@@ -0,0 +1,177 @@
<template>
<div v-if="visible" class="dialog-overlay z-[60]">
<div class="dialog relative">
<div class="text-lg text-center font-light relative z-50">
<div
id="ViewHeading"
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
>
Camera or Other?
</div>
<div
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</div>
<div>
<div class="text-center mt-8">
<div class>
<fa
icon="camera"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
@click="openPhotoDialog()"
/>
</div>
<div class="mt-4">
<input type="file" @change="uploadImageFile" />
</div>
<div class="mt-4">
<span class="mt-2">
... or paste a URL:
<input type="text" v-model="imageUrl" class="border-2" />
</span>
<span class="ml-2">
<fa
v-if="imageUrl"
icon="check"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md cursor-pointer"
@click="acceptUrl"
/>
<!-- so that there's no shifting when it becomes visible -->
<fa v-else icon="check" class="text-white bg-white px-2 py-2" />
</span>
</div>
</div>
</div>
</div>
</div>
<PhotoDialog ref="photoDialog" />
</template>
<script lang="ts">
import axios from "axios";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import PhotoDialog from "@/components/PhotoDialog.vue";
import { NotificationIface } from "@/constants/app";
const inputImageFileNameRef = ref<Blob>();
@Component({
components: { PhotoDialog },
})
export default class ImageMethodDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
claimType: string;
crop: boolean = false;
imageCallback: (imageUrl?: string) => void = () => {};
imageUrl?: string;
visible = false;
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
this.claimType = claimType;
this.crop = !!crop;
this.imageCallback = setImageFn;
this.visible = true;
}
openPhotoDialog(blob?: Blob, fileName?: string) {
this.visible = false;
(this.$refs.photoDialog as PhotoDialog).open(
this.imageCallback,
this.claimType,
this.crop,
blob,
fileName,
);
}
async uploadImageFile(event: Event) {
this.visible = false;
inputImageFileNameRef.value = event.target.files[0];
// https://developer.mozilla.org/en-US/docs/Web/API/File
// ... plus it has a `type` property from my testing
const file = inputImageFileNameRef.value;
if (file != null) {
const reader = new FileReader();
reader.onload = async (e) => {
const data = e.target?.result as ArrayBuffer;
if (data) {
const blob = new Blob([new Uint8Array(data)], {
type: file.type,
});
this.openPhotoDialog(blob, file.name as string);
}
};
reader.readAsArrayBuffer(file as Blob);
}
}
async acceptUrl() {
this.visible = false;
if (this.crop) {
try {
const urlBlobResponse: Blob = await axios.get(this.imageUrl as string, {
responseType: "blob", // This ensures the data is returned as a Blob
});
const fullUrl = new URL(this.imageUrl as string);
const fileName = fullUrl.pathname.split("/").pop() as string;
(this.$refs.photoDialog as PhotoDialog).open(
this.imageCallback,
this.claimType,
this.crop,
urlBlobResponse.data as Blob,
fileName,
);
} catch (error) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error retrieving that image.",
},
5000,
);
}
} else {
this.imageCallback(this.imageUrl);
}
}
close() {
this.visible = false;
}
}
</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: 700px;
}
</style>

View File

@@ -0,0 +1,337 @@
<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"
data-testId="inputDescription"
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 mt-2">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2"
@click="changeUnitCode()"
>
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
</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
data-testId="inputOfferAmount"
type="number"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="amountInput"
/>
<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="mt-4 flex justify-center">
<span>
<router-link
:to="{
name: 'offer-details',
query: {
amountInput,
description,
offererDid: activeDid,
projectId,
projectName,
recipientDid,
recipientName,
unitCode: amountUnitCode,
},
}"
class="text-blue-500"
>
Conditions & more options...
</router-link>
</span>
</div>
<p class="text-center mt-6 mb-2 italic">
Sign & Send to publish to the world
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel"
>
Cancel
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { createAndSubmitOffer } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component
export default class OfferDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop projectId?;
@Prop projectName?;
activeDid = "";
apiServer = "";
amountInput = "0";
amountUnitCode = "HUR";
description = "";
expirationDateInput = "";
recipientDid? = "";
recipientName? = "";
visible = false;
libsUtil = libsUtil;
async open(recipientDid?: string, recipientName?: string) {
try {
this.recipientDid = recipientDid;
this.recipientName = recipientName;
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.error("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,
);
}
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.libsUtil.UNIT_SHORT);
const index = units.indexOf(this.amountUnitCode);
this.amountUnitCode = units[(index + 1) % units.length];
}
increment() {
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
}
decrement() {
this.amountInput = `${Math.max(
0,
(parseFloat(this.amountInput) || 1) - 1,
)}`;
}
cancel() {
this.close();
this.eraseValues();
}
eraseValues() {
this.description = "";
this.amountInput = "0";
this.amountUnitCode = "HUR";
}
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.amountInput),
this.amountUnitCode,
this.expirationDateInput,
).then(() => {
this.description = "";
this.amountInput = "0";
});
}
/**
*
* @param description may be an empty string
* @param hours may be 0
* @param unitCode may be omitted, defaults to "HUR"
*/
public async recordOffer(
description: string,
amount: number,
unitCode: string = "HUR",
expirationDateInput?: string,
) {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record an offer.",
},
-1,
);
return;
}
if (!description && !amount) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
},
-1,
);
return;
}
try {
const result = await createAndSubmitOffer(
this.axios,
this.apiServer,
this.activeDid,
description,
amount,
unitCode,
expirationDateInput,
this.recipientDid,
this.projectId,
);
if (
result.type === "error" ||
this.isOfferCreationError(result.response)
) {
const errorMessage = this.getOfferCreationErrorMessage(result);
console.error("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.error("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,440 @@
<template>
<div v-if="visible" class="dialog-overlay z-[60]">
<div class="dialog relative">
<div class="text-lg text-center font-light relative z-50">
<div
id="ViewHeading"
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none"
>
<span v-if="uploading"> Uploading... </span>
<span v-else-if="blob"> Look Good? </span>
<span v-else> Say "Cheese"! </span>
</div>
<div
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</div>
<div v-if="uploading" class="flex justify-center">
<fa
icon="spinner"
class="fa-spin fa-3x text-center block px-12 py-12"
/>
</div>
<div v-else-if="blob">
<div v-if="crop">
<VuePictureCropper
:boxStyle="{
backgroundColor: '#f8f8f8',
margin: 'auto',
}"
:img="createBlobURL(blob)"
:options="{
viewMode: 1,
dragMode: 'crop',
aspectRatio: 9 / 9,
}"
class="max-h-[90vh] max-w-[90vw] object-contain"
/>
<!-- This gives a round cropper.
:presetMode="{
mode: 'round',
}"
-->
</div>
<div v-else>
<div class="flex justify-center">
<img
:src="createBlobURL(blob)"
class="mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain"
/>
</div>
</div>
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
<button
@click="uploadImage"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
>
<span>Upload</span>
</button>
</div>
<div
v-if="showRetry"
class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
>
<button
@click="retryImage"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
>
<span>Retry</span>
</button>
</div>
</div>
<div v-else ref="cameraContainer">
<!--
Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically:
:resolution="{ width: 375, height: 812 }"
-->
<camera
facingMode="environment"
autoplay
ref="camera"
@started="cameraStarted()"
>
<div
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
>
<button
@click="takeImage()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="camera" class="w-[1em]"></fa>
</button>
</div>
<div
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
>
<button
@click="swapMirrorClass()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="left-right" class="w-[1em]"></fa>
</button>
</div>
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
<button
@click="switchCamera()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="rotate" class="w-[1em]"></fa>
</button>
</div>
</camera>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from "axios";
import Camera from "simple-vue-camera";
import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
@Component({ components: { Camera, VuePictureCropper } })
export default class PhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDeviceNumber = 0;
activeDid = "";
blob?: Blob;
claimType = "";
crop = false;
fileName?: string;
mirror = false;
numDevices = 0;
setImageCallback: (arg: string) => void = () => {};
showRetry = true;
uploading = false;
visible = false;
URL = window.URL || window.webkitURL;
async mounted() {
try {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("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(
setImageFn: (arg: string) => void,
claimType: string,
crop?: boolean,
blob?: Blob, // for image upload, just to use the cropping function
inputFileName?: string,
) {
this.visible = true;
this.claimType = claimType;
this.crop = !!crop;
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) {
bottomNav.style.display = "none";
}
this.setImageCallback = setImageFn;
if (blob) {
this.blob = blob;
this.fileName = inputFileName;
// doesn't make sense to retry the file upload; they can cancel if they picked the wrong one
this.showRetry = false;
} else {
this.blob = undefined;
this.fileName = undefined;
this.showRetry = true;
}
}
close() {
this.visible = false;
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) {
bottomNav.style.display = "";
}
this.blob = undefined;
}
async cameraStarted() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
if (cameraComponent) {
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
this.mirror = cameraComponent.facingMode === "user";
// figure out which device is active
const currentDeviceId = cameraComponent.currentDeviceID();
const devices = await cameraComponent.devices(["videoinput"]);
this.activeDeviceNumber = devices.findIndex(
(device) => device.deviceId === currentDeviceId,
);
}
}
async switchCamera() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
const devices = await cameraComponent?.devices(["videoinput"]);
await cameraComponent?.changeCamera(
devices[this.activeDeviceNumber].deviceId,
);
}
async takeImage(/* payload: MouseEvent */) {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
/**
* This logic to set the image height & width correctly.
* Without it, the portrait orientation ends up with an image that is stretched horizontally.
* Note that it's the same with raw browser Javascript; see the "drawImage" example below.
* Now that I've done it, I can't explain why it works.
*/
let imageHeight = cameraComponent?.resolution?.height;
let imageWidth = cameraComponent?.resolution?.width;
const initialImageRatio = imageWidth / imageHeight;
const windowRatio = window.innerWidth / window.innerHeight;
if (initialImageRatio > 1 && windowRatio < 1) {
// the image is wider than it is tall, and the window is taller than it is wide
// For some reason, mobile in portrait orientation renders a horizontally-stretched image.
// We're gonna force it opposite.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
} else if (initialImageRatio < 1 && windowRatio > 1) {
// the image is taller than it is wide, and the window is wider than it is tall
// Haven't seen this happen, but we'll do it just in case.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
}
const newImageRatio = imageWidth / imageHeight;
if (newImageRatio < windowRatio) {
// the image is a taller ratio than the window, so fit the height first
imageHeight = window.innerHeight / 2;
imageWidth = imageHeight * newImageRatio;
} else {
// the image is a wider ratio than the window, so fit the width first
imageWidth = window.innerWidth / 2;
imageHeight = imageWidth / newImageRatio;
}
// The resolution is only necessary because of that mobile portrait-orientation case.
// The mobile emulation in a browser shows something stretched vertically, but real devices work fine.
this.blob =
(await cameraComponent?.snapshot({
height: imageHeight,
width: imageWidth,
})) || undefined;
// png is default
this.fileName = "snapshot.png";
if (!this.blob) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error taking the picture. Please try again.",
},
5000,
);
return;
}
}
private createBlobURL(blob: Blob): string {
return URL.createObjectURL(blob);
}
async retryImage() {
this.blob = undefined;
}
/****
Here's an approach to photo capture without a library. It has similar quirks.
Now that we've fixed styling for simple-vue-camera, it's not critical to refactor. Maybe someday.
<button id="start-camera" @click="cameraClicked">Start Camera</button>
<video id="video" width="320" height="240" autoplay></video>
<button id="snap-photo" @click="photoSnapped">Snap Photo</button>
<canvas id="canvas" width="320" height="240"></canvas>
async cameraClicked() {
const video = document.querySelector("#video");
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
if (video instanceof HTMLVideoElement) {
video.srcObject = stream;
}
}
photoSnapped() {
const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas");
if (
canvas instanceof HTMLCanvasElement &&
video instanceof HTMLVideoElement
) {
canvas
?.getContext("2d")
?.drawImage(video, 0, 0, canvas.width, canvas.height);
// ... or set the blob:
// canvas?.toBlob(
// (blob) => {
// this.blob = blob;
// },
// "image/jpeg",
// 1,
// );
// data url of the image
const image_data_url = canvas?.toDataURL("image/jpeg");
}
}
****/
async uploadImage() {
this.uploading = true;
if (this.crop) {
this.blob = (await cropper?.getBlob()) || undefined;
}
const token = await accessToken(this.activeDid);
const headers = {
Authorization: "Bearer " + token,
// axios fills in Content-Type of multipart/form-data
};
const formData = new FormData();
if (!this.blob) {
// yeah, this should never happen, but it helps with subsequent type checking
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error finding the picture. Please try again.",
},
5000,
);
this.uploading = false;
return;
}
formData.append("image", this.blob, this.fileName || "snapshot.png");
formData.append("claimType", this.claimType);
try {
const response = await axios.post(
DEFAULT_IMAGE_API_SERVER + "/image",
formData,
{ headers },
);
this.uploading = false;
this.close();
this.setImageCallback(response.data.url as string);
} catch (error) {
console.error("Error uploading the image", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error saving the picture.",
},
5000,
);
this.uploading = false;
this.blob = undefined;
}
}
swapMirrorClass() {
this.mirror = !this.mirror;
if (this.mirror) {
(this.$refs.cameraContainer as HTMLElement).classList.add("mirror-video");
} else {
(this.$refs.cameraContainer as HTMLElement).classList.remove(
"mirror-video",
);
}
}
}
</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: 700px;
}
.mirror-video {
transform: scaleX(-1);
-webkit-transform: scaleX(-1); /* For Safari */
-moz-transform: scaleX(-1); /* For Firefox */
-ms-transform: scaleX(-1); /* For IE */
-o-transform: scaleX(-1); /* For Opera */
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<a
v-if="linkToFull && imageUrl"
:href="imageUrl"
target="_blank"
class="h-full w-full object-contain"
>
<div v-html="generateIdenticon()" class="h-full w-full object-contain" />
</a>
<div
v-else
v-html="generateIdenticon()"
class="h-full w-full object-contain"
/>
</template>
<script lang="ts">
import { toSvg } from "jdenticon";
import { Vue, Component, Prop } from "vue-facing-decorator";
const BLANK_CONFIG = {
lightness: {
color: [1.0, 1.0],
grayscale: [1.0, 1.0],
},
saturation: {
color: 0.0,
grayscale: 0.0,
},
backColor: "#0000",
};
@Component
export default class ProjectIcon extends Vue {
@Prop entityId = "";
@Prop iconSize = 0;
@Prop imageUrl = "";
@Prop linkToFull = false;
generateIdenticon() {
if (this.imageUrl) {
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
} else {
const config = this.entityId ? undefined : BLANK_CONFIG;
const svgString = toSvg(this.entityId, this.iconSize, config);
return svgString;
}
}
}
</script>
<style scoped></style>

View File

@@ -1,7 +1,7 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<ul class="flex text-2xl p-2 gap-2 max-w-3xl mx-auto">
<!-- Home Feed -->
<li
:class="{
@@ -12,7 +12,7 @@
}"
>
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
<fa icon="house-chimney" class="fa-fw" />
</router-link>
</li>
<!-- Search -->
@@ -28,7 +28,7 @@
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
<fa icon="magnifying-glass" class="fa-fw" />
</router-link>
</li>
<!-- Projects -->
@@ -44,7 +44,7 @@
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
<fa icon="hand" class="fa-fw" />
</router-link>
</li>
<!-- Contacts -->
@@ -60,7 +60,7 @@
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
<fa icon="users" class="fa-fw" />
</router-link>
</li>
<!-- Profile -->
@@ -76,7 +76,7 @@
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
<fa icon="circle-user" class="fa-fw" />
</router-link>
</li>
</ul>

View File

@@ -0,0 +1,62 @@
<template>
<div class="absolute right-5 top-3">
<span class="align-center text-red-500 mr-2">{{ message }}</span>
<span class="ml-2">
<router-link
:to="{ name: 'help' }"
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
>
Help
</router-link>
</span>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component
export default class TopMessage extends Vue {
$notify!: (notification: NotificationIface, 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

@@ -1,12 +1,11 @@
import axios from "axios";
import * as R from "ramda";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js";
import { accountsDB, db } from "@/db";
import { db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { getHeaders } from "@/libs/endorserServer";
const ANIMATION_DURATION_SECS = 10;
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
@@ -19,17 +18,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer;
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const headers = {
"Content-Type": "application/json",
};
const identity = JSON.parse(account?.identity || "null");
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
const headers = await getHeaders(activeDid);
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const resp = await axios.get(url, { headers: headers });

View File

@@ -1,12 +1,57 @@
/**
* Generic strings that could be used throughout the app.
*
* See also ../libs/veramo/setup.ts
*/
export enum AppString {
APP_NAME = "Kick-Start with Time",
// This is used in titles and verbiage inside the app.
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
APP_NAME = "Time Safari",
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",
PROD_IMAGE_API_SERVER = "https://image-api.timesafari.app",
TEST_IMAGE_API_SERVER = "https://test-image-api.timesafari.app",
LOCAL_IMAGE_API_SERVER = "http://localhost:3001",
NO_CONTACT_NAME = "(no name)",
}
export const DEFAULT_ENDORSER_API_SERVER =
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
AppString.TEST_ENDORSER_API_SERVER;
export const DEFAULT_IMAGE_API_SERVER =
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
AppString.TEST_IMAGE_API_SERVER;
export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host;
export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED =
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
/**
* The possible values for "group" and "type" are in App.vue.
* From the notiwind package
*/
export interface NotificationIface {
group: string; // "alert" | "modal"
type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string;
text?: string;
noText?: string;
onCancel?: (stopAsking: boolean) => Promise<void>;
onNo?: (stopAsking: boolean) => Promise<void>;
onYes?: () => Promise<void>;
promptToStopAsking?: boolean;
yesText?: string;
}

View File

@@ -1,69 +1,59 @@
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";
// a separate DB because the seed is super-sensitive data
type SensitiveTables = {
accounts: Table<Account>;
};
import { Temp, TempSchema } from "./tables/temp";
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>;
temp: Table<Temp>;
};
/**
* In order to make the next line be acceptable, the program needs to have its linter suppress a rule:
* https://typescript-eslint.io/rules/no-unnecessary-type-constraint/
*
* and change *any* to *unknown*
*
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
*/
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
const SensitiveSchemas = Object.assign({}, AccountsSchema);
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
BaseDexie & T;
// Initialize Dexie databases for sensitive and non-sensitive data
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
/**
* Needed to enable a special webpack setting to allow *await* below:
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
*/
/**
* Create password and place password in localStorage.
*
* It's good practice to keep the data encrypted at rest, so we'll do that even
* if the secret is stored right next to the app.
*/
// Manage the encryption key. If not present in localStorage, create and store it.
const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
if (!localStorage.getItem("secret")) localStorage.setItem("secret", secret);
if (localStorage.getItem("secret") == null) {
localStorage.setItem("secret", secret);
}
// Apply encryption to the sensitive database using the secret key
encrypted(accountsDB, { secretKey: secret });
accountsDB.version(1).stores(SensitiveSchemas);
db.version(1).stores(NonsensitiveSchemas);
// Define the schemas for our databases
// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning
accountsDB.version(1).stores(AccountsSchema);
// v1 also had contacts & settings
// v2 added Log
db.version(2).stores({
...ContactSchema,
...LogSchema,
...SettingsSchema,
});
// v3 added Temp
db.version(3).stores(TempSchema);
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
db.on("populate", function () {
// ensure there's an initial entry for settings
db.settings.add({
// Event handler to initialize the non-sensitive database with default settings
db.on("populate", async () => {
await db.settings.add({
id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
apiServer: DEFAULT_ENDORSER_API_SERVER,
});
});

View File

@@ -1,17 +1,55 @@
/**
* Represents an account stored in the database.
*/
export type Account = {
id?: number; // auto-generated by Dexie
/**
* Auto-generated ID by Dexie
*/
id?: number;
/**
* The date the account was created
*/
dateCreated: string;
derivationPath: string;
/**
* The derivation path for the account, if this is from a mnemonic
*/
derivationPath?: string;
/**
* Decentralized Identifier (DID) for the account
*/
did: string;
// stringified JSON containing underlying key material of type IIdentifier
// https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
identity: string;
/**
* Stringified JSON containing underlying key material, if generated from a mnemonic
* Based on the IIdentifier type from Veramo
* @see {@link https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts}
*/
identity?: string;
/**
* The mnemonic phrase for the account, if this is from a mnemonic
*/
mnemonic?: string;
/**
* The Webauthn credential ID in hex, if this is from a passkey
*/
passkeyCredIdHex?: string;
/**
* The public key in hexadecimal format
*/
publicKeyHex: string;
mnemonic: string;
};
// mark encrypted field by starting with a $ character
// see https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon
/**
* Schema for the accounts table in the database.
* Fields starting with a $ character are encrypted.
* @see {@link https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon}
*/
export const AccountsSchema = {
accounts:
"++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",

View File

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

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

@@ -0,0 +1,11 @@
export interface Log {
date: string;
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", // definitely don't key by the potentially large message field
};

View File

@@ -1,17 +1,65 @@
// a singleton
export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
lastViewedClaimId?: string;
showContactGivesInline?: boolean;
/**
* BoundingBox type describes the geographical bounding box coordinates.
*/
export type BoundingBox = {
eastLong: number; // Eastern longitude
maxLat: number; // Maximum (Northernmost) latitude
minLat: number; // Minimum (Southernmost) latitude
westLong: number; // Western longitude
};
/**
* Settings type encompasses user-specific configuration details.
*/
export type Settings = {
id: number; // Only one entry, keyed with MASTER_SETTINGS_KEY
activeDid?: string; // Active Decentralized ID
apiServer?: string; // API server URL
filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
firstName?: string; // user's full name
hideRegisterPromptOnNewContact?: boolean;
isRegistered?: boolean;
lastName?: string; // deprecated - put all names in firstName
lastNotifiedClaimId?: string;
lastViewedClaimId?: string;
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
profileImageUrl?: string;
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;
}>;
showContactGivesInline?: boolean; // Display contact inline or not
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server
webPushServer?: string; // Web Push server URL
};
export function isAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible);
}
/**
* Schema for the Settings table in the database.
*/
export const SettingsSchema = {
settings: "id",
};
/**
* Constants.
*/
export const MASTER_SETTINGS_KEY = 1;
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;

14
src/db/tables/temp.ts Normal file
View File

@@ -0,0 +1,14 @@
// for ephemeral uses, eg. passing a blob from the service worker to the main thread
export type Temp = {
id: string;
blob?: Blob; // deprecated because webkit (Safari) does not support Blob
blobB64?: string; // base64-encoded blob
};
/**
* Schema for the Temp table in the database.
*/
export const TempSchema = {
temp: "id",
};

View File

@@ -1,13 +1,19 @@
import { IIdentifier } from "@veramo/core";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { getRandomBytesSync } from "ethereum-cryptography/random";
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/76798669'/0'/0'/0'";
import {
createEndorserJwtForDid,
ENDORSER_JWT_URL_LOCATION,
} from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { decodeEndorserJwt } from "@/libs/crypto/vc";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
export const LOCAL_KMS_NAME = "local";
/**
*
@@ -29,7 +35,7 @@ export const newIdentifier = (
keys: [
{
kid: publicHex,
kms: "local",
kms: LOCAL_KMS_NAME,
meta: { derivationPath: derivationPath },
privateKeyHex: privateHex,
publicKeyHex: publicHex,
@@ -62,6 +68,10 @@ export const deriveAddress = (
return [address, privateHex, publicHex, derivationPath];
};
export const generateRandomBytes = (numBytes: number): Uint8Array => {
return getRandomBytesSync(numBytes);
};
/**
*
*
@@ -75,78 +85,54 @@ export const generateSeed = (): string => {
};
/**
* Retreive an access token
* Retrieve an access token, or "" if no DID is provided.
*
* @param {IIdentifier} identifier
* @return {*}
*/
export const accessToken = async (identifier: IIdentifier) => {
const did: string = identifier.did;
const privateKeyHex: string = identifier.keys[0].privateKeyHex as string;
const signer = SimpleSigner(privateKeyHex);
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt: string = await didJwt.createJWT(tokenPayload, {
alg,
issuer: did,
signer,
});
return jwt;
};
export const sign = async (privateKeyHex: string) => {
const signer = SimpleSigner(privateKeyHex);
return signer;
export const accessToken = async (did?: string) => {
if (did) {
const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
return createEndorserJwtForDid(did, tokenPayload);
} else {
return "";
}
};
/**
* Copied out of did-jwt since it's deprecated in that library.
*
* The SimpleSigner returns a configured function for signing data.
*
* @example
* const signer = SimpleSigner(process.env.PRIVATE_KEY)
* signer(data, (err, signature) => {
* ...
* })
*
* @param {String} hexPrivateKey a hex encoded private key
* @return {Function} a configured signer function
*/
export function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
return async (data) => {
const signature = (await signer(data)) as string;
return fromJose(signature);
};
}
@return results of uportJwtPayload:
{ iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } }
// from did-jwt/util; see SimpleSigner above
export function fromJose(signature: string): {
r: string;
s: string;
recoveryParam?: number;
} {
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
Note that similar code is also contained in time-safari
*/
export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
let jwtText = jwtUrlText;
const endorserContextLoc = jwtText.indexOf(ENDORSER_JWT_URL_LOCATION);
if (endorserContextLoc > -1) {
jwtText = jwtText.substring(
endorserContextLoc + ENDORSER_JWT_URL_LOCATION.length,
);
}
const r = bytesToHex(signatureBytes.slice(0, 32));
const s = bytesToHex(signatureBytes.slice(32, 64));
const recoveryParam =
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
return { r, s, recoveryParam };
}
// from did-jwt/util; see SimpleSigner above
export function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}
// JWT format: { header, payload, signature, data }
const jwt = decodeEndorserJwt(jwtText);
return jwt.payload;
};
export const nextDerivationPath = (origDerivPath: string) => {
let lastStr = origDerivPath.split("/").slice(-1)[0];
if (lastStr.endsWith("'")) {
lastStr = lastStr.slice(0, -1);
}
const lastNum = parseInt(lastStr, 10);
const newLastNum = lastNum + 1;
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : "");
const newDerivPath = origDerivPath
.split("/")
.slice(0, -1)
.concat([newLastStr])
.join("/");
return newDerivPath;
};

View File

@@ -0,0 +1,96 @@
import { Buffer } from "buffer/";
import { decode as cborDecode } from "cbor-x";
import { bytesToMultibase, multibaseToBytes } from "did-jwt";
import { getWebCrypto } from "@/libs/crypto/vc/passkeyHelpers";
export const PEER_DID_PREFIX = "did:peer:";
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
/**
*
*
* similar code is in crowd-funder-for-time-pwa libs/crypto/vc/passkeyDidPeer.ts verifyJwtWebCrypto
*
* @returns {Promise<boolean>}
*/
export async function verifyPeerSignature(
payloadBytes: Buffer,
issuerDid: string,
signatureBytes: Uint8Array,
): Promise<boolean> {
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
const WebCrypto = await getWebCrypto();
const verifyAlgorithm = {
name: "ECDSA",
hash: { name: "SHA-256" },
};
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
const keyAlgorithm = {
name: "ECDSA",
namedCurve: publicKeyJwk.crv,
};
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
"jwk",
publicKeyJwk,
keyAlgorithm,
false,
["verify"],
);
const verified = await WebCrypto.subtle.verify(
verifyAlgorithm,
publicKeyCryptoKey,
signatureBytes,
payloadBytes,
);
return verified;
}
export function cborToKeys(publicKeyBytes: Uint8Array) {
const jwkObj = cborDecode(publicKeyBytes);
if (
jwkObj[1] != 2 || // kty "EC"
jwkObj[3] != -7 || // alg "ES256"
jwkObj[-1] != 1 || // crv "P-256"
jwkObj[-2].length != 32 || // x
jwkObj[-3].length != 32 // y
) {
throw new Error("Unable to extract key.");
}
const publicKeyJwk = {
alg: "ES256",
crv: "P-256",
kty: "EC",
x: arrayToBase64Url(jwkObj[-2]),
y: arrayToBase64Url(jwkObj[-3]),
};
const publicKeyBuffer = Buffer.concat([
Buffer.from(jwkObj[-2]),
Buffer.from(jwkObj[-3]),
]);
return { publicKeyJwk, publicKeyBuffer };
}
export function toBase64Url(anythingB64: string) {
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export function arrayToBase64Url(anything: Uint8Array) {
return toBase64Url(Buffer.from(anything).toString("base64"));
}
export function peerDidToPublicKeyBytes(did: string) {
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length));
}
export function createPeerDid(publicKeyBytes: Uint8Array) {
// https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
const methodSpecificId = bytesToMultibase(
publicKeyBytes,
"base58btc",
"p256-pub",
);
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
}

112
src/libs/crypto/vc/index.ts Normal file
View File

@@ -0,0 +1,112 @@
/**
* Verifiable Credential & DID functions, specifically for EndorserSearch.org tools
*
* The goal is to make this folder similar across projects, then move it to a library.
* Other projects: endorser-ch, image-api
*
*/
import * as didJwt from "did-jwt";
import { JWTDecoded } from "did-jwt/lib/JWT";
import { IIdentifier } from "@veramo/core";
import * as u8a from "uint8arrays";
import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer";
export const ETHR_DID_PREFIX = "did:ethr:";
/**
* Meta info about a key
*/
export interface KeyMeta {
/**
* Decentralized ID for the key
*/
did: string;
/**
* Stringified IIDentifier object from Veramo
*/
identity?: string;
/**
* The Webauthn credential ID in hex, if this is from a passkey
*/
passkeyCredIdHex?: string;
}
/**
* Tell whether a key is from a passkey
* @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
*/
export function isFromPasskey(keyMeta?: KeyMeta): boolean {
return !!keyMeta?.passkeyCredIdHex;
}
export async function createEndorserJwtForKey(
account: KeyMeta,
payload: object,
) {
if (account?.identity) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const identity: IIdentifier = JSON.parse(account.identity!);
const privateKeyHex = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex as string);
return didJwt.createJWT(payload, {
issuer: account.did,
signer: signer,
});
} else if (account?.passkeyCredIdHex) {
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
} else {
throw new Error("No identity data found to sign for DID " + account.did);
}
}
/**
* Copied out of did-jwt since it's deprecated in that library.
*
* The SimpleSigner returns a configured function for signing data.
*
* @example
* const signer = SimpleSigner(privateKeyHexString)
* signer(data, (err, signature) => {
* ...
* })
*
* @param {String} hexPrivateKey a hex encoded private key
* @return {Function} a configured signer function
*/
function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
return async (data) => {
const signature = (await signer(data)) as string;
return fromJose(signature);
};
}
// from did-jwt/util; see SimpleSigner above
function fromJose(signature: string): {
r: string;
s: string;
recoveryParam?: number;
} {
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
);
}
const r = bytesToHex(signatureBytes.slice(0, 32));
const s = bytesToHex(signatureBytes.slice(32, 64));
const recoveryParam =
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
return { r, s, recoveryParam };
}
// from did-jwt/util; see SimpleSigner above
function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}
export function decodeEndorserJwt(jwt: string): JWTDecoded {
return didJwt.decodeJWT(jwt);
}

View File

@@ -0,0 +1,539 @@
import { Buffer } from "buffer/";
import { JWTPayload } from "did-jwt";
import { DIDResolutionResult } from "did-resolver";
import { sha256 } from "ethereum-cryptography/sha256.js";
import {
startAuthentication,
startRegistration,
} from "@simplewebauthn/browser";
import {
generateAuthenticationOptions,
generateRegistrationOptions,
verifyAuthenticationResponse,
verifyRegistrationResponse,
} from "@simplewebauthn/server";
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse";
import {
Base64URLString,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
} from "@simplewebauthn/types";
import { AppString } from "@/constants/app";
import { unwrapEC2Signature } from "@/libs/crypto/vc/passkeyHelpers";
import {
arrayToBase64Url,
cborToKeys,
peerDidToPublicKeyBytes,
verifyPeerSignature,
} from "@/libs/crypto/vc/didPeer";
export interface JWK {
kty: string;
crv: string;
x: string;
y: string;
}
export async function registerCredential(passkeyName?: string) {
const options: PublicKeyCredentialCreationOptionsJSON =
await generateRegistrationOptions({
rpName: AppString.APP_NAME,
rpID: window.location.hostname,
userName: passkeyName || AppString.APP_NAME + " User",
// Don't prompt users for additional information about the authenticator
// (Recommended for smoother UX)
attestationType: "none",
authenticatorSelection: {
// Defaults
residentKey: "preferred",
userVerification: "preferred",
// Optional
authenticatorAttachment: "platform",
},
});
// someday, instead of simplwebauthn, we'll go direct: navigator.credentials.create with PublicKeyCredentialCreationOptions
// with pubKeyCredParams: { type: "public-key", alg: -7 }
const attResp = await startRegistration(options);
const verification = await verifyRegistrationResponse({
response: attResp,
expectedChallenge: options.challenge,
expectedOrigin: window.location.origin,
expectedRPID: window.location.hostname,
});
// references for parsing auth data and getting the public key
// https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
// https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
// https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7
const credIdBase64Url = verification.registrationInfo?.credentialID as string;
if (attResp.rawId !== credIdBase64Url) {
console.log("Warning! The raw ID does not match the credential ID.");
}
const credIdHex = Buffer.from(
base64URLStringToArrayBuffer(credIdBase64Url),
).toString("hex");
const { publicKeyJwk } = cborToKeys(
verification.registrationInfo?.credentialPublicKey as Uint8Array,
);
return {
authData: verification.registrationInfo?.attestationObject,
credIdHex: credIdHex,
publicKeyJwk: publicKeyJwk,
publicKeyBytes: verification.registrationInfo
?.credentialPublicKey as Uint8Array,
};
}
export class PeerSetup {
public authenticatorData?: ArrayBuffer;
public challenge?: Uint8Array;
public clientDataJsonBase64Url?: Base64URLString;
public signature?: Base64URLString;
public async createJwtSimplewebauthn(
issuerDid: string,
payload: object,
credIdHex: string,
expMinutes: number = 1,
) {
const credentialId = arrayBufferToBase64URLString(
Buffer.from(credIdHex, "hex").buffer,
);
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = {
...payload,
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
// const payloadHash: Uint8Array = sha256(this.challenge);
const options: PublicKeyCredentialRequestOptionsJSON =
await generateAuthenticationOptions({
challenge: this.challenge,
rpID: window.location.hostname,
allowCredentials: [{ id: credentialId }],
});
// console.log("simple authentication options", options);
const clientAuth = await startAuthentication(options);
// console.log("simple credential get", clientAuth);
const authenticatorDataBase64Url = clientAuth.response.authenticatorData;
this.authenticatorData = Buffer.from(
clientAuth.response.authenticatorData,
"base64",
).buffer;
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON;
// console.log("simple authenticatorData for signing", this.authenticatorData);
this.signature = clientAuth.response.signature;
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
const headerBase64 = Buffer.from(JSON.stringify(header))
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
const dataInJwtString = JSON.stringify(dataInJwt);
const payloadBase64 = Buffer.from(dataInJwtString)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const signature = clientAuth.response.signature;
return headerBase64 + "." + payloadBase64 + "." + signature;
}
public async createJwtNavigator(
issuerDid: string,
payload: object,
credIdHex: string,
expMinutes: number = 1,
) {
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = {
...payload,
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
const dataToSignString = JSON.stringify(fullPayload);
const dataToSignBuffer = Buffer.from(dataToSignString);
const credentialId = Buffer.from(credIdHex, "hex");
// console.log("lower credentialId", credentialId);
this.challenge = new Uint8Array(dataToSignBuffer);
const options = {
publicKey: {
allowCredentials: [
{
id: credentialId,
type: "public-key" as const,
},
],
challenge: this.challenge.buffer,
rpID: window.location.hostname,
userVerification: "preferred" as const,
},
};
const credential = await navigator.credentials.get(options);
// console.log("nav credential get", credential);
this.authenticatorData = credential?.response.authenticatorData;
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
this.authenticatorData as ArrayBuffer,
);
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
credential?.response.clientDataJSON,
);
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
const headerBase64 = Buffer.from(JSON.stringify(header))
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
exp: expiryTime,
iat: issuedAt,
iss: issuerDid,
};
const dataInJwtString = JSON.stringify(dataInJwt);
const payloadBase64 = Buffer.from(dataInJwtString)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const origSignature = Buffer.from(credential?.response.signature).toString(
"base64",
);
this.signature = origSignature
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature;
return jwt;
}
// To use this, add the asn1-ber library and add this import:
// import asn1 from "asn1-ber";
//
// return a low-level signing function, similar to createJWS approach
// async webAuthnES256KSigner(credentialID: string) {
// return async (data: string | Uint8Array) => {
// // get signature from WebAuthn
// const signature = await this.generateWebAuthnSignature(data);
//
// // This converts from the browser ArrayBuffer to a Node.js Buffer, which is a requirement for the asn1 library.
// const signatureBuffer = Buffer.from(signature);
// console.log("lower signature inside signer", signature);
// console.log("lower buffer signature inside signer", signatureBuffer);
// console.log("lower base64 buffer signature inside signer", signatureBuffer.toString("base64"));
// // Decode the DER-encoded signature to extract R and S values
// const reader = new asn1.BerReader(signatureBuffer);
// console.log("lower after reader");
// reader.readSequence();
// console.log("lower after read sequence");
// const r = reader.readString(asn1.Ber.Integer, true);
// console.log("lower after r");
// const s = reader.readString(asn1.Ber.Integer, true);
// console.log("lower after r & s");
//
// // Ensure R and S are 32 bytes each
// const rBuffer = Buffer.from(r);
// const sBuffer = Buffer.from(s);
// console.log("lower after rBuffer & sBuffer", rBuffer, sBuffer);
// const rWithoutPrefix = rBuffer.length > 32 ? rBuffer.slice(1) : rBuffer;
// const sWithoutPrefix = sBuffer.length > 32 ? sBuffer.slice(1) : sBuffer;
// const rPadded =
// rWithoutPrefix.length < 32
// ? Buffer.concat([Buffer.alloc(32 - rWithoutPrefix.length), rBuffer])
// : rWithoutPrefix;
// const sPadded =
// rWithoutPrefix.length < 32
// ? Buffer.concat([Buffer.alloc(32 - sWithoutPrefix.length), sBuffer])
// : sWithoutPrefix;
//
// // Concatenate R and S to form the 64-byte array (ECDSA signature format expected by JWT)
// const combinedSignature = Buffer.concat([rPadded, sPadded]);
// console.log(
// "lower combinedSignature",
// combinedSignature.length,
// combinedSignature,
// );
//
// const combSig64 = combinedSignature.toString("base64");
// console.log("lower combSig64", combSig64);
// const combSig64Url = combSig64
// .replace(/\+/g, "-")
// .replace(/\//g, "_")
// .replace(/=+$/, "");
// console.log("lower combSig64Url", combSig64Url);
// return combSig64Url;
// };
// }
}
export async function createDidPeerJwt(
did: string,
credIdHex: string,
payload: object,
): Promise<string> {
const peerSetup = new PeerSetup();
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex);
return jwt;
}
// I'd love to use this but it doesn't verify.
// Requires:
// npm install @noble/curves
// ... and this import:
// import { p256 } from "@noble/curves/p256";
export async function verifyJwtP256(
credIdHex: string,
issuerDid: string,
authenticatorData: ArrayBuffer,
challenge: Uint8Array,
clientDataJsonBase64Url: Base64URLString,
signature: Base64URLString,
) {
const authDataFromBase = Buffer.from(authenticatorData);
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
const sigBuffer = Buffer.from(signature, "base64");
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
// Hash the client data
const hash = sha256(clientDataFromBase);
// Construct the preimage
const preimage = Buffer.concat([authDataFromBase, hash]);
const isValid = p256.verify(
finalSigBuffer,
new Uint8Array(preimage),
publicKeyBytes,
);
return isValid;
}
export async function verifyJwtSimplewebauthn(
credIdHex: string,
issuerDid: string,
authenticatorData: ArrayBuffer,
challenge: Uint8Array,
clientDataJsonBase64Url: Base64URLString,
signature: Base64URLString,
) {
const authData = arrayToBase64Url(Buffer.from(authenticatorData));
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
const credId = arrayBufferToBase64URLString(
Buffer.from(credIdHex, "hex").buffer,
);
const authOpts: VerifyAuthenticationResponseOpts = {
authenticator: {
credentialID: credId,
credentialPublicKey: publicKeyBytes,
counter: 0,
},
expectedChallenge: arrayToBase64Url(challenge),
expectedOrigin: window.location.origin,
expectedRPID: window.location.hostname,
response: {
authenticatorAttachment: "platform",
clientExtensionResults: {},
id: credId,
rawId: credId,
response: {
authenticatorData: authData,
clientDataJSON: clientDataJsonBase64Url,
signature: signature,
},
type: "public-key",
},
};
const verification = await verifyAuthenticationResponse(authOpts);
return verification.verified;
}
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
export async function verifyJwtWebCrypto(
credId: Base64URLString,
issuerDid: string,
authenticatorData: ArrayBuffer,
challenge: Uint8Array,
clientDataJsonBase64Url: Base64URLString,
signature: Base64URLString,
) {
const authDataFromBase = Buffer.from(authenticatorData);
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
const sigBuffer = Buffer.from(signature, "base64");
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
// Hash the client data
const hash = sha256(clientDataFromBase);
// Construct the preimage
const preimage = Buffer.concat([authDataFromBase, hash]);
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
if (!did.startsWith("did:peer:0z")) {
throw new Error(
"This only verifies a peer DID, method 0, encoded base58btc.",
);
}
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
// (another reference is the @aviarytech/did-peer resolver)
/**
* Looks like JsonWebKey2020 isn't too difficult:
* - change context security/suites link to jws-2020/v1
* - change publicKeyMultibase to publicKeyJwk generated with cborToKeys
* - change type to JsonWebKey2020
*/
const id = did.split(":")[2];
const multibase = id.slice(1);
const encnumbasis = multibase.slice(1);
const didDocument = {
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1",
],
assertionMethod: [did + "#" + encnumbasis],
authentication: [did + "#" + encnumbasis],
capabilityDelegation: [did + "#" + encnumbasis],
capabilityInvocation: [did + "#" + encnumbasis],
id: did,
keyAgreement: undefined,
service: undefined,
verificationMethod: [
{
controller: did,
id: did + "#" + encnumbasis,
publicKeyMultibase: multibase,
type: "EcdsaSecp256k1VerificationKey2019",
},
],
};
return {
didDocument,
didDocumentMetadata: {},
didResolutionMetadata: { contentType: "application/did+ld+json" },
};
}
// convert COSE public key to PEM format
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function COSEtoPEM(cose: Buffer) {
// const alg = cose.get(3); // Algorithm
const x = cose[-2]; // x-coordinate
const y = cose[-3]; // y-coordinate
// Ensure the coordinates are in the correct format
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error because it complains about the type of x and y
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
// Convert to PEM format
const pem = `-----BEGIN PUBLIC KEY-----
${pubKeyBuffer.toString("base64")}
-----END PUBLIC KEY-----`;
return pem;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlDecode(input: string) {
input = input.replace(/-/g, "+").replace(/_/g, "/");
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
const str = atob(input + pad);
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes.buffer;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlEncode(buffer: ArrayBuffer) {
const str = String.fromCharCode(...new Uint8Array(buffer));
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// from @simplewebauthn/browser
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer);
let str = "";
for (const charCode of bytes) {
str += String.fromCharCode(charCode);
}
const base64String = btoa(str);
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
// from @simplewebauthn/browser
function base64URLStringToArrayBuffer(base64URLString: string) {
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/");
const padLength = (4 - (base64.length % 4)) % 4;
const padded = base64.padEnd(base64.length + padLength, "=");
const binary = atob(padded);
const buffer = new ArrayBuffer(binary.length);
const bytes = new Uint8Array(buffer);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return buffer;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function pemToCryptoKey(pem: string) {
const binaryDerString = atob(
pem
.split("\n")
.filter((x) => !x.includes("-----"))
.join(""),
);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
// console.log("binaryDer", binaryDer.buffer);
return await window.crypto.subtle.importKey(
"spki",
binaryDer.buffer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
true,
["verify"],
);
}

View File

@@ -0,0 +1,105 @@
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts
import { AsnParser } from "@peculiar/asn1-schema";
import { ECDSASigValue } from "@peculiar/asn1-ecc";
/**
* In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
*
* See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
*/
export function unwrapEC2Signature(signature: Uint8Array): Uint8Array {
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
let rBytes = new Uint8Array(parsedSignature.r);
let sBytes = new Uint8Array(parsedSignature.s);
if (shouldRemoveLeadingZero(rBytes)) {
rBytes = rBytes.slice(1);
}
if (shouldRemoveLeadingZero(sBytes)) {
sBytes = sBytes.slice(1);
}
const finalSignature = isoUint8ArrayConcat([rBytes, sBytes]);
return finalSignature;
}
/**
* Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence
* should be removed based on the following logic:
*
* "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0,
* then remove the leading 0x0 byte"
*/
function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
}
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoUint8Array.ts#L49
/**
* Combine multiple Uint8Arrays into a single Uint8Array
*/
export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
let pointer = 0;
const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0);
const toReturn = new Uint8Array(totalLength);
arrays.forEach((arr) => {
toReturn.set(arr, pointer);
pointer += arr.length;
});
return toReturn;
}
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts
let webCrypto: { subtle: SubtleCrypto } | undefined = undefined;
export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
/**
* Hello there! If you came here wondering why this method is asynchronous when use of
* `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this
* synchronous. For example, `generateRegistrationOptions()` and `generateAuthenticationOptions()`
* become synchronous if we make this synchronous (since nothing else in that method is async)
* which represents a breaking API change in this library's core API.
*
* TODO: If it's after February 2025 when you read this then consider whether it still makes sense
* to keep this method asynchronous.
*/
const toResolve: Promise<{ subtle: SubtleCrypto }> = new Promise(
(resolve, reject) => {
if (webCrypto) {
return resolve(webCrypto);
}
/**
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
* support (and Node v20+)
*/
const _globalThisCrypto =
_getWebCryptoInternals.stubThisGlobalThisCrypto();
if (_globalThisCrypto) {
webCrypto = _globalThisCrypto;
return resolve(webCrypto);
}
// We tried to access it both in Node and globally, so bail out
return reject(new MissingWebCrypto());
},
);
return toResolve;
}
class MissingWebCrypto extends Error {
constructor() {
const message = "An instance of the Crypto API could not be located";
super(message);
this.name = "MissingWebCrypto";
}
}
// Make it possible to stub return values during testing
const _getWebCryptoInternals = {
stubThisGlobalThisCrypto: () => globalThis.crypto,
// Make it possible to reset the `webCrypto` at the top of the file
setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => {
webCrypto = newCrypto;
},
};

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,393 @@
// many of these are also found in endorser-mobile utility.ts
import axios, { AxiosResponse } from "axios";
import { Buffer } from "buffer";
import * as R from "ramda";
import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY,
} from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import {
containsHiddenDid,
GenericCredWrapper,
GenericVerifiableCredential,
OfferVerifiableCredential,
} from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer";
import { KeyMeta } from "@/libs/crypto/vc";
import { createPeerDid } from "@/libs/crypto/vc/didPeer";
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
export const PRIVACY_MESSAGE =
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64";
/* eslint-disable prettier/prettier */
export const UNIT_SHORT: Record<string, string> = {
"BTC": "BTC",
"BX": "BX",
"ETH": "ETH",
"HUR": "Hours",
"USD": "US $",
};
/* eslint-enable prettier/prettier */
/* eslint-disable prettier/prettier */
export const UNIT_LONG: Record<string, string> = {
"BTC": "Bitcoin",
"BX": "Buxbe",
"ETH": "Ethereum",
"HUR": "hours",
"USD": "dollars",
};
/* eslint-enable prettier/prettier */
const UNIT_CODES: Record<string, Record<string, string>> = {
BTC: {
name: "Bitcoin",
faIcon: "bitcoin-sign",
},
HUR: {
name: "hours",
faIcon: "clock",
},
USD: {
name: "US Dollars",
faIcon: "dollar",
},
};
export function iconForUnitCode(unitCode: string) {
return UNIT_CODES[unitCode]?.faIcon || "question";
}
// from https://stackoverflow.com/a/175787/845494
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
//
export function isNumeric(str: string): boolean {
// This ignore commentary is because typescript complains when you pass a string to isNaN.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return !isNaN(str) && !isNaN(parseFloat(str));
}
export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}
export const isGlobalUri = (uri: string) => {
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
};
export const isGiveAction = (
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
) => {
return veriClaim.claimType === "GiveAction";
};
export const nameForDid = (
activeDid: string,
contacts: Array<Contact>,
did: string,
): string => {
if (did === activeDid) {
return "you";
}
const contact = R.find((con) => con.did == did, contacts);
return nameForContact(contact);
};
export const nameForContact = (
contact?: Contact,
capitalize?: boolean,
): string => {
return (
(contact?.name as string) ||
(capitalize ? "This" : "this") + " unnamed user"
);
};
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
};
/**
* @returns true if the user can confirm the claim
* @param veriClaim is expected to have fields: claim, claimType, and issuer
*/
export const isGiveRecordTheUserCanConfirm = (
isRegistered: boolean,
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
activeDid: string,
confirmerIdList: string[] = [],
) => {
return (
isRegistered &&
isGiveAction(veriClaim) &&
!confirmerIdList.includes(activeDid) &&
veriClaim.issuer !== activeDid &&
!containsHiddenDid(veriClaim.claim)
);
};
export async function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string); // potential problem if it returns an ArrayBuffer?
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
// Extract the content type and the Base64 data
const [metadata, base64] = base64DataUrl.split(",");
const contentTypeMatch = metadata.match(/data:(.*?);base64/);
const contentType = contentTypeMatch ? contentTypeMatch[1] : "";
const byteCharacters = atob(base64);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: contentType });
}
/**
* @returns the DID of the person who offered, or undefined if hidden
* @param veriClaim is expected to have fields: claim and issuer
*/
export const offerGiverDid: (
arg0: GenericCredWrapper<OfferVerifiableCredential>,
) => string | undefined = (veriClaim) => {
let giver;
if (
veriClaim.claim.offeredBy?.identifier &&
!serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
) {
giver = veriClaim.claim.offeredBy.identifier;
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
giver = veriClaim.issuer;
}
return giver;
};
/**
* @returns true if the user can fulfill the offer
* @param veriClaim is expected to have fields: claim, claimType, and issuer
*/
export const canFulfillOffer = (
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
) => {
return !!(
veriClaim.claimType === "Offer" &&
offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
);
};
// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
export function findAllVisibleToDids(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
input: any,
humanReadable = false,
): Record<string, Array<string>> {
if (Array.isArray(input)) {
const result: Record<string, Array<string>> = {};
for (let i = 0; i < input.length; i++) {
const inside = findAllVisibleToDids(input[i], humanReadable);
for (const key in inside) {
const pathKey = humanReadable
? "#" + (i + 1) + " " + key
: "[" + i + "]" + key;
result[pathKey] = inside[key];
}
}
return result;
} else if (input instanceof Object) {
// regular map (non-array) object
const result: Record<string, Array<string>> = {};
for (const key in input) {
if (key.endsWith("VisibleToDids")) {
const newKey = key.slice(0, -"VisibleToDids".length);
const pathKey = humanReadable ? newKey : "." + newKey;
result[pathKey] = input[key];
} else {
const inside = findAllVisibleToDids(input[key], humanReadable);
for (const insideKey in inside) {
const pathKey = humanReadable
? key + "'s " + insideKey
: "." + key + insideKey;
result[pathKey] = inside[insideKey];
}
}
}
return result;
} else {
return {};
}
}
/**
* Test findAllVisibleToDids
*
pkgx +deno.land sh
deno
import * as R from 'ramda';
//import { findAllVisibleToDids } from './src/libs/util'; // doesn't work because other dependencies fail so gotta copy-and-paste function
console.log(R.equals(findAllVisibleToDids(null), {}));
console.log(R.equals(findAllVisibleToDids(9), {}));
console.log(R.equals(findAllVisibleToDids([]), {}));
console.log(R.equals(findAllVisibleToDids({}), {}));
console.log(R.equals(findAllVisibleToDids({ issuer: "abc" }), {}));
console.log(R.equals(findAllVisibleToDids({ issuerVisibleToDids: ["abc"] }), { ".issuer": ["abc"] }));
console.log(R.equals(findAllVisibleToDids([{ issuerVisibleToDids: ["abc"] }]), { "[0].issuer": ["abc"] }));
console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] } }]), { "[1].fluff.issuer": ["abc"] }));
console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] }, stuff: [ { did: "HIDDEN", agentDidVisibleToDids: ["def", "ghi"] } ] }]), { "[1].fluff.issuer": ["abc"], "[1].stuff[0].agentDid": ["def", "ghi"] }));
*
**/
export interface AccountKeyInfo extends Account, KeyMeta {}
export const getAccount = async (
activeDid: string,
): Promise<AccountKeyInfo | undefined> => {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
return account;
};
/**
* Generates a new identity, saves it to the database, and sets it as the active identity.
* @return {Promise<string>} with the DID of the new identity
*/
export const generateSaveAndActivateIdentity = async (): Promise<string> => {
const mnemonic = generateSeed();
// address is 0x... ETH address, without "did:eth:"
const [address, privateHex, publicHex, derivationPath] =
deriveAddress(mnemonic);
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
const identity = JSON.stringify(newId);
await accountsDB.open();
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: derivationPath,
did: newId.did,
identity: identity,
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
return newId.did;
};
export const registerAndSavePasskey = async (
keyName: string,
): Promise<Account> => {
const cred = await registerCredential(keyName);
const publicKeyBytes = cred.publicKeyBytes;
const did = createPeerDid(publicKeyBytes as Uint8Array);
const passkeyCredIdHex = cred.credIdHex as string;
const account = {
dateCreated: new Date().toISOString(),
did,
passkeyCredIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
};
await accountsDB.open();
await accountsDB.accounts.add(account);
return account;
};
export const registerSaveAndActivatePasskey = async (
keyName: string,
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
return account;
};
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const passkeyExpirationSeconds =
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
60;
return passkeyExpirationSeconds;
};
export const sendTestThroughPushServer = async (
subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean,
): Promise<AxiosResponse> => {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
const newPayload = {
// eslint-disable-next-line prettier/prettier
message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
...subscriptionJSON,
};
console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload);
const response = await axios.post(
pushUrl + "/web-push/send-test",
payloadStr,
{
headers: {
"Content-Type": "application/json",
},
},
);
console.log("Got response from web push server:", response);
return response;
};

View File

@@ -1,151 +1,7 @@
// Created from the setup in https://veramo.io/docs/guides/react_native
// see also ../constants/app.ts and
// Core interfaces
/* import {
createAgent,
IDIDManager,
IResolver,
IDataStore,
IKeyManager,
} from "@veramo/core";
*/
// Core identity manager plugin
//import { DIDManager } from "@veramo/did-manager";
// Ethr did identity provider
//import { EthrDIDProvider } from "@veramo/did-provider-ethr";
// Core key manager plugin
//import { KeyManager } from "@veramo/key-manager";
// Custom key management system for RN
//import { KeyManagementSystem } from '@veramo/kms-local-react-native'
// Custom resolver
// Custom resolvers
//import { DIDResolverPlugin } from "@veramo/did-resolver";
/* import { Resolver } from "did-resolver";
import { getResolver as ethrDidResolver } from "ethr-did-resolver";
import { getResolver as webDidResolver } from "web-did-resolver";
*/
// for VCs and VPs https://veramo.io/docs/api/credential-w3c
//import { CredentialIssuer } from '@veramo/credential-w3c'
// Storage plugin using TypeOrm
/* import {
Entities,
KeyStore,
DIDStore,
IDataStoreORM,
} from "@veramo/data-store";
*/
// TypeORM is installed with @veramo/typeorm
//import { createConnection } from 'typeorm'
//import * as R from "ramda";
/*
import { Contact } from '../entity/contact'
import { Settings } from '../entity/settings'
import { PrivateData } from '../entity/privateData'
import { Initial1616938713828 } from '../migration/1616938713828-initial'
import { SettingsContacts1616967972293 } from '../migration/1616967972293-settings-contacts'
import { EncryptedSeed1637856484788 } from '../migration/1637856484788-EncryptedSeed'
import { HomeScreenConfig1639947962124 } from '../migration/1639947962124-HomeScreenConfig'
import { HandlePublicKeys1652142819353 } from '../migration/1652142819353-HandlePublicKeys'
import { LastClaimsSeen1656811846836 } from '../migration/1656811846836-LastClaimsSeen'
import { ContactRegistered1662256903367 }from '../migration/1662256903367-ContactRegistered'
import { PrivateData1663080623479 } from '../migration/1663080623479-PrivateData'
const ALL_ENTITIES = Entities.concat([Contact, Settings, PrivateData])
// Create react native DB connection configured by ormconfig.js
export const dbConnection = createConnection({
database: 'endorser-mobile.sqlite',
entities: ALL_ENTITIES,
location: 'default',
logging: ['error', 'info', 'warn'],
migrations: [ Initial1616938713828, SettingsContacts1616967972293, EncryptedSeed1637856484788, HomeScreenConfig1639947962124, HandlePublicKeys1652142819353, LastClaimsSeen1656811846836, ContactRegistered1662256903367, PrivateData1663080623479 ],
migrationsRun: true,
type: 'react-native',
})
*/
function didProviderName(netName: string) {
return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName);
}
//const NETWORK_NAMES = ["mainnet", "rinkeby"];
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
DEFAULT_DID_PROVIDER_NETWORK_NAME,
);
export const HANDY_APP = false;
// this is used as the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch";
//const INFURA_PROJECT_ID = "INFURA_PROJECT_ID";
/*
const providers = {}
NETWORK_NAMES.forEach((networkName) => {
providers[didProviderName(networkName)] = new EthrDIDProvider({
defaultKms: 'local',
network: networkName,
rpcUrl: 'https://' + networkName + '.infura.io/v3/' + INFURA_PROJECT_ID,
gas: 1000001,
ttl: 60 * 60 * 24 * 30 * 12 + 1,
})
})
const didManager = new DIDManager({
store: new DIDStore(dbConnection),
defaultProvider: DEFAULT_DID_PROVIDER_NAME,
providers: providers,
})
*/
/* const basicDidResolvers = NETWORK_NAMES.map((networkName) => [
networkName,
new Resolver({
ethr: ethrDidResolver({
networks: [
{
name: networkName,
rpcUrl:
"https://" + networkName + ".infura.io/v3/" + INFURA_PROJECT_ID,
},
],
}).ethr,
web: webDidResolver().web,
}),
]);
const basicResolverMap = R.fromPairs(basicDidResolvers)
export const DEFAULT_BASIC_RESOLVER = basicResolverMap[DEFAULT_DID_PROVIDER_NETWORK_NAME]
const agentDidResolvers = NETWORK_NAMES.map((networkName) => {
return new DIDResolverPlugin({
resolver: basicResolverMap[networkName],
})
})
let allPlugins = [
new CredentialIssuer(),
new KeyManager({
store: new KeyStore(dbConnection),
kms: {
local: new KeyManagementSystem(),
},
}),
didManager,
].concat(agentDidResolvers)
*/
//export const agent = createAgent<IDIDManager & IKeyManager & IDataStore & IDataStoreORM & IResolver>({ plugins: allPlugins })
export const DEFAULT_DID_PROVIDER_NAME = didProviderName("mainnet");

View File

@@ -1,5 +1,5 @@
import { createPinia } from "pinia";
import { createApp } from "vue";
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
@@ -11,12 +11,22 @@ import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
@@ -26,28 +36,43 @@ import {
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImagePortrait,
faLeftRight,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQuestion,
faQrcode,
faRotate,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
@@ -56,12 +81,22 @@ import {
} from "@fortawesome/free-solid-svg-icons";
library.add(
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
@@ -71,28 +106,43 @@ library.add(
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImagePortrait,
faLeftRight,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQrcode,
faQuestion,
faRotate,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
@@ -101,11 +151,40 @@ library.add(
);
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Camera from "simple-vue-camera";
createApp(App)
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
function setupGlobalErrorHandler(app: VueApp) {
// @ts-expect-error 'cause we cannot see why config is not defined
app.config.errorHandler = (
err: Error,
instance: ComponentPublicInstance | null,
info: string,
) => {
console.error(
"Ouch! Global Error Handler. Info:",
info,
"Error:",
err,
"Instance:",
instance,
);
// Want to show a nice notiwind notification but can't figure out how.
alert(
(err.message || "Something bad happened") +
" - Try reloading or restarting the app.",
);
};
}
const app = createApp(App)
.component("fa", FontAwesomeIcon)
.component("camera", Camera)
.use(createPinia())
.use(VueAxios, axios)
.use(router)
.use(Notifications)
.mount("#app");
.use(Notifications);
setupGlobalErrorHandler(app);
app.mount("#app");

View File

@@ -2,8 +2,9 @@
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
// NODE_ENV is "production" by default with "vite build". See https://vitejs.dev/guide/env-and-mode
if (import.meta.env.NODE_ENV === "production") {
register("/sw_scripts-combined.js", {
ready() {
console.log(
"App is being served from cache by a service worker.\n" +

View File

@@ -1,5 +1,11 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db";
import {
createRouter,
createWebHistory,
NavigationGuardNext,
RouteLocationNormalized,
RouteRecordRaw,
} from "vue-router";
import { accountsDB } from "@/db/index";
/**
*
@@ -7,7 +13,11 @@ import { accountsDB } from "@/db";
* @param from :RouteLocationNormalized
* @param next :NavigationGuardNext
*/
const enterOrStart = async (to, from, next) => {
const enterOrStart = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
@@ -18,179 +28,204 @@ const enterOrStart = async (to, from, next) => {
};
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: () =>
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/account",
name: "account",
component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
beforeEnter: enterOrStart,
component: () => import("../views/AccountViewView.vue"),
},
{
path: "/claim/:id?",
name: "claim",
component: () => import("../views/ClaimView.vue"),
},
{
path: "/claim-add-raw/:id?",
name: "claim-add-raw",
component: () => import("../views/ClaimAddRawView.vue"),
},
{
path: "/confirm-contact",
name: "confirm-contact",
component: () =>
import(
/* webpackChunkName: "confirm-contact" */ "../views/ConfirmContactView.vue"
),
component: () => import("../views/ConfirmContactView.vue"),
},
{
path: "/confirm-gift/:id?",
name: "confirm-gift",
component: () => import("@/views/ConfirmGiftView.vue"),
},
{
path: "/contact-amounts",
name: "contact-amounts",
component: () =>
import(
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
),
component: () => import("../views/ContactAmountsView.vue"),
},
{
path: "/contact-gift",
name: "contact-gift",
component: () => import("../views/ContactGiftingView.vue"),
},
{
path: "/contact-import",
name: "contact-import",
component: () => import("../views/ContactImportView.vue"),
},
{
path: "/contact-qr",
name: "contact-qr",
component: () =>
import(
/* webpackChunkName: "contact-qr" */ "../views/ContactQRScanShowView.vue"
),
component: () => import("../views/ContactQRScanShowView.vue"),
},
{
path: "/contacts",
name: "contacts",
component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
beforeEnter: enterOrStart,
component: () => import("../views/ContactsView.vue"),
},
{
path: "/scan-contact",
name: "scan-contact",
component: () =>
import(
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
),
path: "/did/:did?",
name: "did",
component: () => import("../views/DIDView.vue"),
},
{
path: "/discover",
name: "discover",
component: () =>
import(/* webpackChunkName: "discover" */ "../views/DiscoverView.vue"),
component: () => import("../views/DiscoverView.vue"),
},
{
path: "/gifted-details",
name: "gifted-details",
component: () => import("@/views/GiftedDetailsView.vue"),
},
{
path: "/help",
name: "help",
component: () =>
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
component: () => import("../views/HelpView.vue"),
},
{
path: "/import-account",
name: "import-account",
component: () =>
import(
/* webpackChunkName: "import-account" */ "../views/ImportAccountView.vue"
),
path: "/help-notifications",
name: "help-notifications",
component: () => import("../views/HelpNotificationsView.vue"),
},
{
path: "/import-derive",
name: "import-derive",
component: () =>
import(
/* webpackChunkName: "import-derive" */ "../views/ImportDerivedAccountView.vue"
),
path: "/help-onboarding",
name: "help-onboarding",
component: () => import("../views/HelpOnboardingView.vue"),
},
{
path: "/new-edit-account",
name: "new-edit-account",
component: () =>
import(
/* webpackChunkName: "new-edit-account" */ "../views/NewEditAccountView.vue"
),
},
{
path: "/new-edit-commitment",
name: "new-edit-commitment",
component: () =>
import(
/* webpackChunkName: "new-edit-commitment" */ "../views/NewEditCommitmentView.vue"
),
},
{
path: "/new-edit-project",
name: "new-edit-project",
component: () =>
import(
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
),
},
{
path: "/new-identifier",
name: "new-identifier",
component: () =>
import(
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
),
path: "/",
name: "home",
component: () => import("../views/HomeView.vue"),
},
{
path: "/identity-switcher",
name: "identity-switcher",
component: () =>
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
component: () => import("../views/IdentitySwitcherView.vue"),
},
{
path: "/project",
path: "/import-account",
name: "import-account",
component: () => import("../views/ImportAccountView.vue"),
},
{
path: "/import-derive",
name: "import-derive",
component: () => import("../views/ImportDerivedAccountView.vue"),
},
{
path: "/new-edit-account",
name: "new-edit-account",
component: () => import("../views/NewEditAccountView.vue"),
},
{
path: "/new-edit-project",
name: "new-edit-project",
component: () => import("../views/NewEditProjectView.vue"),
},
{
path: "/new-identifier",
name: "new-identifier",
component: () => import("../views/NewIdentifierView.vue"),
},
{
path: "/offer-details/:id?",
name: "offer-details",
component: () => import("../views/OfferDetailsView.vue"),
},
{
path: "/project/:id?",
name: "project",
component: () =>
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
component: () => import("../views/ProjectViewView.vue"),
},
{
path: "/projects",
name: "projects",
component: () =>
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
component: () => import("../views/ProjectsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/quick-action-bvc",
name: "quick-action-bvc",
component: () => import("../views/QuickActionBvcView.vue"),
},
{
path: "/quick-action-bvc-begin",
name: "quick-action-bvc-begin",
component: () => import("../views/QuickActionBvcBeginView.vue"),
},
{
path: "/quick-action-bvc-end",
name: "quick-action-bvc-end",
component: () => import("../views/QuickActionBvcEndView.vue"),
},
{
path: "/scan-contact",
name: "scan-contact",
component: () => import("../views/ContactScanView.vue"),
},
{
path: "/search-area",
name: "search-area",
component: () => import("../views/SearchAreaView.vue"),
},
{
path: "/seed-backup",
name: "seed-backup",
component: () =>
import(
/* webpackChunkName: "seed-backup" */ "../views/SeedBackupView.vue"
),
component: () => import("../views/SeedBackupView.vue"),
},
{
path: "/shared-photo",
name: "shared-photo",
component: () => import("@/views/SharedPhotoView.vue"),
},
// /share-target is also an endpoint in the service worker
{
path: "/start",
name: "start",
component: () =>
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
component: () => import("../views/StartView.vue"),
},
{
path: "/statistics",
name: "statistics",
component: () =>
import(
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
),
component: () => import("../views/StatisticsView.vue"),
},
{
path: "/contact-gives",
name: "contact-gives",
component: () =>
import(
/* webpackChunkName: "statistics" */ "../views/ContactGiftingView.vue"
),
path: "/test",
name: "test",
component: () => import("../views/TestView.vue"),
},
];
/** @type {*} */
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
const errorHandler = (error, to, from) => {
const errorHandler = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any,
to: RouteLocationNormalized,
from: RouteLocationNormalized,
) => {
// Handle the error here
console.error("Caught in top level error handler:", error, to, from);

View File

@@ -2,10 +2,13 @@ import axios from "axios";
import * as didJwt from "did-jwt";
import { AppString } from "@/constants/app";
import { db } from "../db";
import { SERVICE_ID } from "../libs/veramo/setup";
import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
/**
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
*/
export async function testServerRegisterUser() {
const testUser0Mnem =
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";

2185
src/util.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- 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" />
</button>
Raw Claim
</h1>
</div>
<div class="flex">
<textarea rows="20" class="border-2 w-full" v-model="claimStr"></textarea>
</div>
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="submitClaim()"
>
Sign &amp; Send
</button>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
@Component({
components: { QuickNav },
})
export default class ClaimAddRawView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = "";
apiServer = "";
claimStr = "";
async mounted() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.claimStr = (this.$route as Router).query["claim"];
try {
this.veriClaim = JSON.parse(this.claimStr);
this.claimStr = JSON.stringify(this.veriClaim, null, 2);
} catch (e) {
// ignore a parse
}
}
async submitClaim() {
const fullClaim = JSON.parse(this.claimStr);
const result = await serverUtil.createAndSubmitClaim(
fullClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Claim submitted.",
},
5000,
);
} else {
console.error("Got error submitting the claim:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the claim. See logs for more info.",
},
-1,
);
}
}
}
</script>

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

@@ -0,0 +1,889 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- 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" />
</button>
Verifiable Claim Details
</h1>
</div>
<!-- Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div class="block flex gap-4 overflow-hidden">
<div class="overflow-hidden">
<h2 class="text-md font-bold">
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
<button
v-if="
['GiveAction', 'Offer'].includes(
veriClaim.claimType as string,
) && veriClaim.issuer === activeDid
"
@click="onClickEditClaim"
title="Edit"
data-testId="editClaimButton"
>
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</button>
</h2>
<div class="text-sm">
<div>
{{ veriClaim.id }}
<button
@click="
libsUtil.doCopyTwoSecRedo(
veriClaim.id as string,
() => (showIdCopy = !showIdCopy),
)
"
class="ml-2 mr-2"
>
<fa icon="copy" class="text-slate-400 fa-fw" />
</button>
<span v-show="showIdCopy">Copied ID</span>
</div>
<div data-testId="description">
<fa icon="message" class="fa-fw text-slate-400" />
{{
veriClaim.claim?.itemOffered?.description ||
veriClaim.claim?.description
}}
</div>
<div>
<fa icon="user" class="fa-fw text-slate-400" />
{{ veriClaim.issuer }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(veriClaim.issuer)">
<button
@click="
libsUtil.doCopyTwoSecRedo(
veriClaim.issuer as string,
() => (showDidCopy = !showDidCopy),
)
"
class="ml-2 mr-2"
>
<fa icon="copy" class="text-slate-400 fa-fw" />
</button>
<span v-show="showDidCopy">Copied DID</span>
</span>
</div>
<div>
<fa icon="calendar" class="fa-fw text-slate-400" />
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
</div>
<div v-if="veriClaim.claim.image" class="flex justify-center">
<a :href="veriClaim.claim.image" target="_blank">
<img :src="veriClaim.claim.image" class="h-24 rounded-xl" />
</a>
</div>
<!-- Fullfills Links -->
<!-- fullfills links for a give -->
<div v-if="detailsForGive?.fulfillsPlanHandleId">
<router-link
:to="
'/project/' +
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
"
class="text-blue-500 mt-2"
>
Fulfills a bigger plan...
</router-link>
</div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div
v-if="
detailsForGive?.fulfillsType &&
detailsForGive?.fulfillsType !== 'PlanAction' &&
detailsForGive?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path -->
<a
@click="
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
"
class="text-blue-500 mt-4"
>
Fulfills
{{
capitalizeAndInsertSpacesBeforeCaps(
detailsForGive.fulfillsType,
)
}}...
</a>
</div>
<!-- fullfills links for an offer -->
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
<router-link
:to="
'/project/' +
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
"
class="text-blue-500 mt-4"
>
Offered to a bigger plan...
</router-link>
</div>
</div>
</div>
</div>
</div>
<div class="mt-8">
<button
v-if="libsUtil.canFulfillOffer(veriClaim)"
@click="openFulfillGiftDialog()"
class="col-span-1 block w-fit text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
>
Affirm Delivery
<fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" />
</button>
</div>
<div v-if="libsUtil.isGiveAction(veriClaim)">
<div class="flex columns-3">
<button
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md"
v-if="
libsUtil.isGiveRecordTheUserCanConfirm(
isRegistered,
veriClaim,
activeDid,
confirmerIdList,
)
"
@click="confirmConfirmClaim()"
>
Confirm
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
</button>
<h2 v-else class="font-bold uppercase text-xl mt-2">Confirmations</h2>
<span class="mt-0.5 px-4 py-2">
<router-link
v-if="libsUtil.isGiveAction(veriClaim)"
:to="'/confirm-gift/' + encodeURIComponent(veriClaim.id)"
class="col-span-1 text-blue-500"
data-testId="confirmGiftLink"
>
Details...
</router-link>
</span>
</div>
<GiftedDialog ref="customGiveDialog" />
<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 class="ml-4">
<li
v-for="confirmerId in confirmerIdList"
:key="confirmerId"
class="list-disc ml-4"
>
<div class="flex gap-4">
<div class="grow overflow-hidden">
<div class="text-sm">
{{ didInfo(confirmerId) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
<button
@click="
copyToClipboard(
'The DID of ' + confirmerId,
confirmerId,
)
"
>
<fa icon="copy" class="text-slate-400 fa-fw" />
</button>
</span>
</div>
</div>
</div>
</li>
</ul>
</div>
<!--
Never need to show this message:
"Nobody that you know can see someone who has confirmed this claim."
If there is nobody in the confirmerIdList then we'll show the combined "nobody" message.
If there is somebody in the confirmerIdList then that's all they need to show.
-->
<!-- Now show anyone linked to confirmers. -->
<div v-if="confsVisibleToIdList.length > 0">
The following people can connect you with people who have issued or
confirmed this claim.
<ul class="ml-4">
<li
v-for="confsVisibleTo in confsVisibleToIdList"
:key="confsVisibleTo"
class="list-disc ml-4"
>
<div class="flex gap-4">
<div class="grow overflow-hidden">
<div class="text-sm">
{{ didInfo(confsVisibleTo) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
<button
@click="
copyToClipboard(
'The DID of ' + confsVisibleTo,
confsVisibleTo,
)
"
>
<fa icon="copy" class="text-slate-400 fa-fw" />
</button>
</span>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
<!-- explain if user cannot confirm -->
<!-- Note that these conditions are mirrored in userCanConfirm(). -->
<div v-if="confirmerIdList.includes(activeDid)">
You have confirmed this claim.
</div>
<div v-else-if="veriClaim.issuer == activeDid">
You cannot confirm this because you issued this claim, so you already
count as confirming it.
</div>
<div v-else-if="serverUtil.containsHiddenDid(veriClaim.claim)">
You cannot confirm this because it contains hidden identifiers.
</div>
</div>
<div>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">
{{ serverUtil.containsHiddenDid(veriClaim) ? "Visible " : "" }}Details
</h2>
<div
v-if="
serverUtil.containsHiddenDid(veriClaim) &&
R.isEmpty(veriClaimDidsVisible)
"
class="mb-2"
>
Some of the details are not visible to you; they show as "HIDDEN". They
are not visible to any of your direct contacts, either.
<span v-if="canShare">
If you'd like to ask any of your contacts to take a look and see if
their contacts can see more details,
<a @click="onClickShareClaim()" class="text-blue-500"
>click to send them this info</a
>
and see if they are willing to make an introduction. They are surely
connected to someone; if you don't know who to ask, you might try the
person who registered you.
</span>
<span v-else>
If you'd like to ask any of your contacts to take a look and see if
their contacts can see more details,
<a
@click="copyToClipboard('This page location', windowLocation)"
class="text-blue-500"
>share this page with them</a
>
and see if they are willing to make an introduction.
</span>
</div>
<div v-if="!R.isEmpty(veriClaimDidsVisible)">
Some of the details are not visible to you but they are visible to some
of your contacts.
<span v-if="canShare">
If you'd like an introduction,
<a @click="onClickShareClaim()" class="text-blue-500"
>click to share the information with them and ask if they'll tell
you more about the participants.</a
>
</span>
<span v-else>
If you'd like an introduction,
<a
@click="copyToClipboard('Location', windowLocation)"
class="text-blue-500"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
</span>
<div
v-for="(visibleDidPath, index) of Object.keys(veriClaimDidsVisible)"
:key="index"
class="list-disc p-4"
>
<div class="text-sm">
<fa icon="minus" class="fa-fw" />
The {{ visibleDidPath }} is visible to:
</div>
<div class="ml-12 p-1">
<ul>
<li
v-for="(visDid, idx2) of veriClaimDidsVisible[visibleDidPath]"
:key="idx2"
class="list-disc"
>
<div class="text-sm mt-2">
<span>
{{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<button
@click="copyToClipboard('The DID of ' + visDid, visDid)"
>
<fa icon="copy" class="text-slate-400 fa-fw" />
</button>
</span>
<span v-if="veriClaim.publicUrls?.[visDid]"
>, found at
<fa icon="globe" class="fa-fw text-slate-400" />&nbsp;<a
:href="veriClaim.publicUrls?.[visDid]"
class="text-blue-500"
>{{
veriClaim.publicUrls[visDid].substring(
veriClaim.publicUrls[visDid].indexOf("//") + 2,
)
}}
</a>
</span>
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
<span v-if="isEditedGlobalId" class="mt-2">
This record is an edited version. The latest version is here.
</span>
<br />
<button @click="showVeriClaimDump = !showVeriClaimDump" class="ml-2">
Details
<fa v-if="showVeriClaimDump" icon="chevron-up" class="text-blue-400" />
<fa v-else icon="chevron-down" class="text-blue-400" />
</button>
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
<pre
v-if="showVeriClaimDump"
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
>{{ veriClaimDump }}</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-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
@click="showFullClaim(veriClaim.id as string)"
>
Load Full Claim Details
</button>
</div>
<div v-else>
<pre
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
>{{ fullClaimDump }}</pre
>
</div>
<a
:href="apiServer + '/api/claim/' + veriClaim.id"
target="_blank"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
>
View on the Public Server
</a>
</section>
</template>
<script lang="ts">
import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import * as serverUtil from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts";
import {
GenericCredWrapper,
GiverReceiverInputInfo,
OfferVerifiableCredential,
} from "@/libs/endorserServer";
@Component({
components: { GiftedDialog, QuickNav },
})
export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
allMyDids: Array<string> = [];
allContacts: Array<Contact> = [];
apiServer = "";
canShare = false;
confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
confsVisibleErrorMessage = "";
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
detailsForGive = null;
detailsForOffer = null;
fullClaim = null;
fullClaimDump = "";
fullClaimMessage = "";
isEditedGlobalId = false;
isRegistered = false;
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
showDidCopy = false;
showIdCopy = false;
showVeriClaimDump = false;
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
veriClaimDump = "";
veriClaimDidsVisible = {};
windowLocation = window.location.href;
R = R;
yaml = yaml;
libsUtil = libsUtil;
serverUtil = serverUtil;
resetThisValues() {
this.confirmerIdList = [];
this.confsVisibleErrorMessage = "";
this.confsVisibleToIdList = [];
this.detailsForGive = null;
this.detailsForOffer = null;
this.fullClaim = null;
this.fullClaimDump = "";
this.fullClaimMessage = "";
this.isEditedGlobalId = false;
this.isRegistered = false;
this.numConfsNotVisible = 0;
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
this.veriClaimDump = "";
}
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();
this.isRegistered = settings?.isRegistered || false;
await accountsDB.open();
const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did);
const pathParam = window.location.pathname.substring("/claim/".length);
let claimId;
if (pathParam) {
claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, this.activeDid);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "No claim ID was provided.",
},
-1,
);
}
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
// then use this truer check: navigator.canShare && navigator.canShare()
this.canShare = !!navigator.share;
}
// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
capitalizeAndInsertSpacesBeforeCaps(text: string) {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
totalConfirmers() {
return (
this.numConfsNotVisible +
this.confirmerIdList.length +
this.confsVisibleToIdList.length
);
}
// Isn't there a better way to make this available to the template?
didInfo(did: string) {
return serverUtil.didInfo(
did,
this.activeDid,
this.allMyDids,
this.allContacts,
);
}
async loadClaim(claimId: string, userDid: string) {
const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/"
: "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId);
const headers = await serverUtil.getHeaders(userDid);
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.veriClaim = resp.data;
this.veriClaimDump = yaml.dump(this.veriClaim);
this.veriClaimDidsVisible = libsUtil.findAllVisibleToDids(
this.veriClaim,
true,
);
} else {
// actually, axios typically throws an error so we never get here
console.error("Error getting claim:", resp);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem retrieving that claim.",
},
-1,
);
return;
}
this.isEditedGlobalId = !this.veriClaim.handleId.endsWith(claimId);
// retrieve more details on Give, Offer, or Plan
if (this.veriClaim.claimType === "GiveAction") {
const giveUrl =
this.apiServer +
"/api/v2/report/gives?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string);
const giveHeaders = await serverUtil.getHeaders(userDid);
const giveResp = await this.axios.get(giveUrl, {
headers: giveHeaders,
});
if (giveResp.status === 200) {
this.detailsForGive = giveResp.data.data[0];
} else {
console.error("Error getting detailed give info:", giveResp);
}
} else if (this.veriClaim.claimType === "Offer") {
const offerUrl =
this.apiServer +
"/api/v2/report/offers?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string);
const offerHeaders = await serverUtil.getHeaders(userDid);
const offerResp = await this.axios.get(offerUrl, {
headers: offerHeaders,
});
if (offerResp.status === 200) {
this.detailsForOffer = offerResp.data.data[0];
} else {
console.error("Error getting detailed offer info:", offerResp);
}
}
// retrieve the list of confirmers
const confirmUrl =
this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await serverUtil.getHeaders(userDid);
const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders,
});
if (response.status === 200) {
const resultList1 = response.data.result || [];
//const publicUrls = resultList.publicUrls || [];
delete resultList1.publicUrls;
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 claim:", serverError);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving claim data.",
},
-1,
);
}
}
async showFullClaim(claimId: string) {
const url =
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
const headers = await serverUtil.getHeaders(this.activeDid);
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.fullClaim = resp.data;
this.fullClaimDump = yaml.dump(this.fullClaim);
} else {
// actually, axios typically throws an error so we never get here
console.error("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,
);
}
}
}
confirmConfirmClaim() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Confirm",
text: "Do you personally confirm that this is true?",
onYes: async () => {
await this.confirmClaim();
},
},
-1,
);
}
// similar code is found in ProjectViewView
async confirmClaim() {
// 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 = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted.",
},
5000,
);
} else {
console.error("Got error submitting the confirmation:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation. See logs for more info.",
},
-1,
);
}
}
showDifferentClaimPage(claimId: string) {
const route = {
path: "/claim/" + encodeURIComponent(claimId),
};
(this.$router as Router).push(route).then(async () => {
this.resetThisValues();
await this.loadClaim(claimId, this.activeDid);
});
}
openFulfillGiftDialog() {
const giver: GiverReceiverInputInfo = {
did: libsUtil.offerGiverDid(
this.veriClaim as GenericCredWrapper<OfferVerifiableCredential>,
),
};
(this.$refs.customGiveDialog as GiftedDialog).open(
giver,
undefined,
this.veriClaim.handleId,
"Offer fulfilled by " + (giver?.name || "someone not named"),
);
}
copyToClipboard(name: string, text: string) {
useClipboard()
.copy(text)
.then(() => {
this.$notify(
{
group: "alert",
type: "toast",
title: "Copied",
text: (name || "That") + " was copied to the clipboard.",
},
2000,
);
});
}
onClickShareClaim() {
window.navigator.share({
title: "Help Connect Me",
text: "I'm trying to find the full details of this claim. Can you help me?",
url: this.windowLocation,
});
}
onClickEditClaim() {
if (this.veriClaim.claimType === "GiveAction") {
const route = {
name: "gifted-details",
query: {
prevCredToEdit: JSON.stringify(this.veriClaim),
destinationPathAfter:
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
},
};
(this.$router as Router).push(route);
} else if (this.veriClaim.claimType === "Offer") {
const route = {
name: "offer-details",
query: {
prevCredToEdit: JSON.stringify(this.veriClaim),
destinationPathAfter:
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
},
};
(this.$router as Router).push(route);
} else {
console.error(
"Unrecognized claim type for edit:",
this.veriClaim.claimType,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "This is an unrecognized claim type.",
},
3000,
);
}
}
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
@@ -30,17 +30,19 @@
</div>
<div class="mt-8">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Add Contact"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
value="Add Contact"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
</div>
</div>
</section>
</template>

Some files were not shown because too many files have changed in this diff Show More