Compare commits

..

287 Commits

Author SHA1 Message Date
Jose Olarte III
03a3964cbd Merge branch 'master' into test-playwright 2024-08-30 15:37:30 +08:00
46ebd95394 add wording in help page 2024-08-29 20:02:57 -06:00
acf04792be change tests assuming result will be at top 2024-08-29 19:49:35 -06:00
06774469be add blurbs for different audiences in Help, and allow a link for direct search on project discovery page 2024-08-29 19:25:34 -06:00
Jose Olarte III
3262f0ee64 Improved create project test
- Added editing to test
- Added variables for other values
2024-08-29 19:25:02 +08:00
Jose Olarte III
69473a826f Removed test.slow() 2024-08-28 19:32:51 +08:00
Jose Olarte III
51eb45353e Moved common functions to testUtils 2024-08-28 19:12:59 +08:00
Jose Olarte III
b1875b5812 Remove unneeded timeouts 2024-08-28 16:24:50 +08:00
Jose Olarte III
bd27d8a10f Merge branch 'master' into test-playwright 2024-08-28 16:12:52 +08:00
Jose Olarte III
c076ce5b44 Remove PWA test
Created a new branch for this specific test, instead
2024-08-28 16:12:30 +08:00
a7b89f4bb6 bump to version 0.3.21 2024-08-24 08:36:14 -06:00
e9c5bd8e99 after copying personal data, add a message to copy contacts for them 2024-08-24 07:56:23 -06:00
5d47eab6d8 add test for new name-entry & copy-to-clipboard flow 2024-08-24 07:23:49 -06:00
e24cd06e3d make the user-name pop-up the preferred way to set the name 2024-08-24 06:36:49 -06:00
767d33c0a0 prompt for name when showing info, and provide a "copy" page when remote 2024-08-23 20:06:50 -06:00
22c3d28405 move some buttons to take less space at the top of Home 2024-08-23 15:22:48 -06:00
7a25892472 add test for registration of new user 2024-08-22 20:21:37 -06:00
Jose Olarte III
915d51dc2f In-progress: PWA install test 2024-08-22 21:24:05 +08:00
Jose Olarte III
b1d61251dc Merge branch 'master' into test-playwright 2024-08-22 18:05:35 +08:00
8d0cc8e0d1 fix linting 2024-08-21 19:16:00 -06:00
c5c687a9c5 add tests for importing multiple records, fix other confirmation tests 2024-08-21 18:43:28 -06:00
Jose Olarte III
a9aeeeb51e Playwright: Record 10 gives 2024-08-21 19:47:48 +08:00
Jose Olarte III
d679d0c804 Merge branch 'master' into test-playwright 2024-08-21 15:22:22 +08:00
7cba232e44 fix tests 2024-08-20 20:05:04 -06:00
c95b2178ef copy a list of contacts and then import 2024-08-20 19:39:29 -06:00
511be5f9a2 move contact actions into the details page (prepping for checkboxes) 2024-08-19 20:18:06 -06:00
8b4f46d07b bump verison and add "-beta" 2024-08-18 17:51:31 -06:00
4064eb75a9 bump to version 0.3.20 2024-08-18 17:04:14 -06:00
d96aa01107 update bad verbiage on offer page, fix offer test 2024-08-18 17:02:15 -06:00
6e89271616 bump verison and add "-beta" 2024-08-18 14:58:01 -06:00
ee9c14942c bump to version 0.3.19 2024-08-18 14:14:53 -06:00
a8bb1b46c2 fix error editing an offer, tweak tests to fix red in IntelliJ 2024-08-18 14:13:42 -06:00
a8b82037b9 bump to version 0.3.18 2024-08-18 13:52:55 -06:00
5811dacb84 Merge pull request 'offer editing' (#123) from offer-edit into master
Reviewed-on: #123
2024-08-18 15:48:53 -04:00
1a4052d1a0 fix tests, add test for offer update 2024-08-18 13:48:07 -06:00
a9b12f4d7c allow editing of an offer 2024-08-17 19:59:02 -06:00
269d00a096 start with offer-edit 2024-08-16 15:58:54 -06:00
05f898d462 put BTC before BX in unit rotation 2024-08-15 19:41:18 -06:00
2c2c95a824 fix destination page after photo is shared 2024-08-14 08:56:57 -06:00
4244e6b279 add recipient description to offers in user's list 2024-08-12 20:38:54 -06:00
56e3440875 misc commentary 2024-08-12 18:51:41 -06:00
1fe540d5a8 fix list of offers (and some other lists), and add tests for offers 2024-08-12 09:25:01 -06:00
089d4f0733 change back the check for adding a service worker because tests would get constant errors 2024-08-12 09:23:25 -06:00
da79d581b7 bump version and add "-beta" 2024-08-12 09:19:15 -06:00
cefa384ff1 bump to version 0.3.17 2024-08-12 09:17:32 -06:00
Jose Olarte III
c4a8026276 DONE: create 10 projects 2024-08-12 19:54:25 +08:00
Jose Olarte III
10fad9c167 Merge branch 'master' into test-playwright 2024-08-12 16:40:05 +08:00
5849ae2de4 remove "export" that's not available in raw JS 2024-08-11 19:01:34 -06:00
60ed21c0d9 fix image shared with web share 2024-08-11 08:17:27 -06:00
3f77f9b3ff record some info on my attempt to test a service worker 2024-08-10 20:09:49 -06:00
6728cbe93e bump version and add "-beta" 2024-08-10 16:11:28 -06:00
cfb8b7841e bump to version 0.3.16 2024-08-10 16:08:34 -06:00
01a8814b4e fix linting, and give instructions for current test suite 2024-08-10 13:37:31 -06:00
70a1ea362f fix a test, add potential-failing comment 2024-08-10 08:11:31 -06:00
Jose Olarte III
084fb09971 IN-PROGRESS: create 10 projects 2024-08-09 23:04:25 +08:00
fb0219d1d7 add image on entries in a project 2024-08-09 07:55:31 -06:00
024fc6be06 show image on the view-claim screen 2024-08-09 07:27:29 -06:00
041cc30eb8 refactor confirmation section to show together and more cleanly 2024-08-09 07:20:01 -06:00
b8181f6ae3 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
bcc0fac0a8 Playwright: added ID to spinbutton 2024-08-08 15:47:19 +08:00
f724476ed6 tweak instructions for minimal test data 2024-08-07 09:25:30 -06:00
Jose Olarte III
6ca5bf754b (Switch back to test server) 2024-08-07 22:06:36 +08:00
Jose Olarte III
25eaff62d8 Playwright: expended contact test 2024-08-07 21:51:24 +08:00
Jose Olarte III
dd1532e2f4 Playwright: test against created records 2024-08-06 20:12:34 +08:00
Jose Olarte III
7033d259e1 Playwright: added import 2024-08-06 16:20:52 +08:00
Jose Olarte III
aae2e62177 Playwright: removed redundant tests 2024-08-05 19:59:40 +08:00
Jose Olarte III
9a9c2b1813 Playwright: combined no-ID tests 2024-08-05 19:59:25 +08:00
Jose Olarte III
ee75576cda Playwright: implemented importUser 2024-08-05 19:58:40 +08:00
Jose Olarte III
88efa36542 Playwright: importUser function 2024-08-05 19:56:42 +08:00
fe1cd32be1 bump version and add "-beta" 2024-08-04 20:26:56 -06:00
c8f0f2c2b1 bump to version 0.3.15, fix a README instruction 2024-08-04 20:01:26 -06:00
7aaf981b71 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
ca8da9fd5e add 'isRegistered' check to guard against many buttons 2024-08-04 19:56:10 -06:00
ff3d397150 move pointers to other projects up in the project view 2024-08-04 19:09:11 -06:00
052d5c5bd1 add a test for empty ID, fix some linting 2024-08-04 07:30:35 -06:00
9213ad1f4a remove unused code 2024-08-02 20:38:21 -06:00
98f4665465 comment out a breaking test on local data & enhance those instructions 2024-08-02 18:58:39 -06:00
Jose Olarte III
c5f5e81f2c Playwright: check ID generation 2024-07-31 19:44:44 +08:00
Jose Olarte III
26f6eb66fe Playwright: additional checks to add contact 2024-07-30 19:13:18 +08:00
Jose Olarte III
28dd602d9f ID-specific locators 2024-07-30 18:50:53 +08:00
Jose Olarte III
fb0a64c0ab Mirrored browser selection 2024-07-30 16:36:11 +08:00
Jose Olarte III
88f41b885c Added IDs for Playwright targeting 2024-07-30 16:35:55 +08:00
bbf621fb18 make instructions for an Endorser server started from scratch 2024-07-29 19:19:07 -06:00
Jose Olarte III
a0adc1517c Playwright: check usage limits (no-ID and with-ID) 2024-07-29 19:09:56 +08:00
Jose Olarte III
812c8a418e Playwright: confirm contact appears on home feed 2024-07-29 19:09:35 +08:00
Jose Olarte III
2f0326f182 Corrected some test labels 2024-07-29 17:01:50 +08:00
1fbd1da87d add visibility flag set, refactor to see results, and add copy icons for contact info 2024-07-28 20:15:36 -06:00
d96770a351 move copy icon for DIDs on contact screen 2024-07-28 17:53:09 -06:00
8d684f1b29 tweak verbiage and make other UI tweaks 2024-07-28 17:09:57 -06:00
6272b3045b fix where it doesn't remove the plan when editing and removing it 2024-07-28 17:09:06 -06:00
375d6ddbe2 fix problem detecting plans when editing gifts 2024-07-28 17:08:44 -06:00
f497c53294 hide the details of a claim by default 2024-07-27 18:38:52 -06:00
cb8aeeac1b show full contact details, plus other tweaks 2024-07-27 18:22:22 -06:00
6191a4893f add a config for local testing, plus add mobile testing and some instructions 2024-07-27 16:52:44 -06:00
791a35d97c fix one linting error 2024-07-26 19:34:22 -06:00
5647c4627f import & update selected contacts 2024-07-26 19:12:12 -06:00
Jose Olarte III
7dfc377610 Playwright: check no-ID messaging 2024-07-26 18:51:28 +08:00
Jose Olarte III
11a3e981a6 Playwright: check test API 2024-07-26 18:16:20 +08:00
cd04f35224 remove example test file 2024-07-25 18:40:12 -06:00
Jose Olarte III
59f97ffc28 Optimize tests 2024-07-25 16:53:33 +08:00
Jose Olarte III
e20cd2aac1 Merge branch 'master' into test-playwright 2024-07-25 14:41:43 +08:00
3f2f334424 add help text, both in general and for download 2024-07-24 20:06:49 -06:00
3b05ae7d9d enhance seed-backup with clipboard copy & more info 2024-07-24 19:23:25 -06:00
Jose Olarte III
1973ca1977 New test 2024-07-24 22:09:10 +08:00
81c96a5cd1 make the list of all claims show a link to each specific claim 2024-07-23 20:58:19 -06:00
c0df24fe01 add more type casts 2024-07-23 20:57:10 -06:00
c16c1689a3 add ability to edit a GiveAction 2024-07-23 20:14:07 -06:00
Jose Olarte III
262e5cc30f More tests added 2024-07-23 21:26:05 +08:00
6ae2329317 refactor out unused DB reference 2024-07-20 07:24:22 -06:00
f362f19cbb await all of the db.settings updates 2024-07-20 07:19:27 -06:00
Jose Olarte III
c037f95b5e Filename-based sequence 2024-07-20 17:03:57 +08:00
Jose Olarte III
de88626239 Switched to baseURL 2024-07-20 16:36:16 +08:00
Jose Olarte III
3f2163c30a Simplify 2024-07-20 15:16:54 +08:00
Jose Olarte III
538345c07b Check activity feed 2024-07-20 15:15:08 +08:00
Jose Olarte III
df07607b47 Cleanup 2024-07-20 14:55:55 +08:00
e51a7b84a4 fix linting 2024-07-19 21:15:56 -06:00
bfa30d691b Merge branch 'passkey-cache' 2024-07-19 20:53:37 -06:00
403327c25a create an identifier by default, while letting them choose if passkeys are enabled 2024-07-19 20:49:43 -06:00
9361f68888 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: #122
2024-07-19 20:00:06 -04:00
Kent Bull
f1f98417cd docs: add tlmgr font packages 2024-07-19 17:59:54 -06:00
fcef84bc82 rename "docs" directory to "doc" 2024-07-19 14:40:48 -06:00
285f0a5e0e add instructions to run tests, and fix linting (for WebStorm) 2024-07-19 14:35:48 -06:00
bc59cd5c41 cache the passkey JWANT access token for multiple signatures 2024-07-19 12:44:54 -06:00
1172aad318 Merge pull request 'docs: basic pandoc setup' (#118) from kentbull/crowd-funder-for-time-pwa:kb/add-usage-guide into master
Reviewed-on: #118
2024-07-19 12:47:18 -04:00
Jose Olarte III
81e9da5eb0 Create Project automation + test 2024-07-19 19:21:48 +08:00
Jose Olarte III
38ce988b9e Cleanup 2024-07-19 17:40:22 +08:00
Jose Olarte III
2fe865d4ad Updated test directory 2024-07-18 20:00:34 +08:00
Jose Olarte III
c3cf382dbf Tesdt: validate copy contact info to clipboard 2024-07-18 19:56:40 +08:00
Jose Olarte III
4c8846d65a Test: new ID from seed phrase 2024-07-18 19:56:12 +08:00
Jose Olarte III
9536fc9b5e Playwright install 2024-07-18 19:55:57 +08:00
9b65fb7ef9 remove remaining getIdentity calls & fix QR code for did:peer 2024-07-15 20:47:10 -06:00
f74b399871 reword some things in help 2024-07-15 19:11:12 -06:00
05398b4de7 add BTC donation address 2024-07-15 17:18:22 -06:00
2aedf6c185 move low-level DID-related create & decode into separate folder (#120)
Co-authored-by: Trent Larson <trent@trentlarson.com>
Reviewed-on: #120
Co-authored-by: trentlarson <trent@trentlarson.com>
Co-committed-by: trentlarson <trent@trentlarson.com>
2024-07-13 13:24:54 -04:00
bc00eac143 Merge pull request 'Refactor JWT-creation calls through single function' (#119) from passkey-all into master
Reviewed-on: #119
2024-07-11 22:32:30 -04:00
925f3e90bb change first page back to prompts without passkey 2024-07-11 19:54:20 -06:00
bc1846a95a consolidate getIdentity & remove dups 2024-07-11 19:43:56 -06:00
674ca1d63c replace remaining didJwt.createJwt calls with one that checks for did:peer 2024-07-11 19:35:17 -06:00
f184fe4d51 linting cleanup 2024-07-09 19:42:55 -06:00
c67ceebc67 change accessToken to take a DID 2024-07-09 19:20:05 -06:00
c200cdbead add expiration inside JWANT & refactor getHeaders to move toward supporting did:peer 2024-07-09 17:56:48 -06:00
2dd6e9b07a make a passkey-generator in start & home pages, and make that the default 2024-07-06 19:12:31 -06:00
33d6b9df96 misc tweaks and linting clean-up 2024-07-06 13:04:15 -06:00
63d0f3c748 misc syntactic & type-checking clean-up 2024-07-06 07:15:46 -06:00
54d14324a1 allow deletion of an identity 2024-07-05 19:37:45 -06:00
05cc5b011d show a loading indicator on the claim-confirmation screen 2024-07-01 17:55:21 -06:00
a3b0993855 fill in the "Load More" links for plan linkages 2024-06-30 20:10:18 -06:00
596454fc3d add section for gives provided by a plan 2024-06-30 20:06:47 -06:00
5e39b91ee5 fix type of the raw claim sent 2024-06-29 13:32:13 -06:00
dffa007a74 add advanced page & flag for editing raw claims, and fix recipient assignment in detail screen 2024-06-29 10:18:56 -06:00
2a8aa8be78 Merge branch 'master' into kb/add-usage-guide 2024-06-26 13:19:58 -04:00
Kent Bull
23cc923144 docs: finish initial boostrapping dev guide 2024-06-26 11:06:18 -06:00
Kent Bull
38ec7320bb docs: add more docs on local run 2024-06-25 19:30:29 -06:00
Kent Bull
316e4be25a docs: basic pandoc setup 2024-06-25 09:25:58 -06:00
1c0e0aeeba modify & explain icons next to feed 2024-06-25 11:04:40 -04:00
1147ee4707 refactor display logic a bit (no flow changes intended) 2024-06-25 11:04:40 -04:00
e68d4fbe6d passkey test (#116)
Co-authored-by: Trent Larson <trent@trentlarson.com>
Reviewed-on: #116
Co-authored-by: trentlarson <trent@trentlarson.com>
Co-committed-by: trentlarson <trent@trentlarson.com>
2024-06-24 22:21:24 -04:00
c3a1571c2f move & resize the contact edit & info buttons 2024-06-22 12:34:30 -06:00
fafdccae66 bump version and add "-beta" 2024-06-22 12:23:57 -06:00
1611d22892 bump to v 0.3.14 2024-06-22 12:23:10 -06:00
4c6c85983c fix checkbox verbiage when no project is chosen for a give 2024-06-22 12:06:55 -06:00
978a31a34e fix prompt for already-registered contacts (plus some verbiage) 2024-06-22 11:47:10 -06:00
c7c6b7c071 add BX currency, add link for user's activity, tweak verbiage 2024-06-21 20:33:44 -06:00
daf692537c improve messaging when user has no offers or projects 2024-06-21 19:52:35 -06:00
6bc7dfd76d fix justification of checkboxes and text so they don't move 2024-06-21 19:25:46 -06:00
b87142d3ed 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
15256bf698 tweak UI for give-confirmation screen 2024-06-21 16:02:08 -06:00
886e22ba88 add Confirm Gift screen for simpler confirmation 2024-06-20 20:52:26 -06:00
a85aac9630 fix dependency vulnerabilities 2024-05-24 11:42:36 -06:00
766727c799 bump version and add -beta; enhance help 2024-05-24 10:21:08 -06:00
08b67984e4 bump to verson 0.3.13 2024-05-24 10:19:17 -06:00
50bba70e1f allow link to the large version of a project image 2024-05-24 09:11:20 -06:00
64f5656f41 add an image to projects (which shows on all ProjectIcons except for offers) 2024-05-23 20:51:40 -06:00
e3e2031bd8 bump version and add -beta 2024-05-20 08:27:12 -06:00
3cd164b09d update CHANGELOG 2024-05-19 20:03:30 -06:00
141fb39ad1 bump to version 0.3.12 2024-05-19 19:57:02 -06:00
11b662e326 fix the photo share_target, and tweak other verbiage 2024-05-19 19:56:25 -06:00
2de254f9a1 bump version and add -beta 2024-05-19 19:32:38 -06:00
1bf57d3228 add a global error handler 2024-05-19 16:25:44 -06:00
567bcad88d bump to version 0.3.11 (and enhance warning on profile deletion) 2024-05-19 08:39:18 -06:00
9bdb87e9ef set the correct active camera number when it starts 2024-05-17 20:24:33 -06:00
e7e1176a83 bump version and add -beta 2024-05-17 12:16:23 -06:00
7b6afe25c5 allow any image URL for gifts & profiles 2024-05-12 21:43:18 -06:00
a8ef530d58 allow file choice for gift, plus other UI fixes 2024-05-12 17:55:54 -06:00
ee3d4acb58 fix cropping problem where long images go off the screen 2024-05-12 12:39:16 -06:00
36d2e41fea bump to v 0.3.10, fix image upload on Chrome 2024-05-12 12:12:59 -06:00
03ac31d981 Merge pull request 'add a share_target for people to add a photo' (#115) from share-photo into master
Reviewed-on: #115
2024-05-11 20:03:33 -04:00
b81c096fe4 add file-chooser to the profile image selection 2024-05-11 12:30:10 -06:00
6bcc0023cd style the sharing screen (plus other fixes) 2024-05-11 07:09:48 -06:00
aa7d82c531 add a share_target for people to add a photo 2024-05-10 13:17:20 -06:00
a95c398e81 increment version and add "-beta" 2024-04-28 20:10:39 -06:00
874e717e69 bump to version 0.3.9 2024-04-28 20:09:56 -06:00
c107073592 disallow new-project page if not registered 2024-04-28 19:16:29 -06:00
3ea5c42769 remove verbiage on front page that's now extra 2024-04-28 19:04:44 -06:00
9157837586 show something to indicate claims were sent (mostly in BVC screens) 2024-04-28 18:36:06 -06:00
751c066bd0 constantly recheck on home screen if not registered 2024-04-28 17:02:31 -06:00
c403356055 add registration inside contact import, with flag to hide it 2024-04-28 16:18:30 -06:00
009a7ecdf8 add 'registered' flag in contact info 2024-04-28 13:12:26 -06:00
bd148e88a3 for scan on QR code screen, import and keep on that screen 2024-04-27 20:33:10 -06:00
bca5adecc9 add tweaks to testing instructions 2024-04-27 14:59:23 -06:00
73f9d7f9e9 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
6f4876e32b fix problem with duplicates in feed, plus some other UI tweaks 2024-04-26 17:05:11 -06:00
c1fe8216f6 allow loading more gives & offers & plans when limits are hit on project view 2024-04-26 15:44:09 -06:00
56523da11e remove some 'uppercase' CSS markers 2024-04-25 20:17:49 -06:00
35ec7fd43c put button directly on contacts page to show the given totals 2024-04-24 20:38:34 -06:00
94b5389ce9 change remainder of "confirm" calls to better UX 2024-04-24 20:11:38 -06:00
421b4c1719 replace many of the javascript "confirm" calls with the nicer UX version 2024-04-24 19:52:33 -06:00
6ce3a0703c remove 'moment' library that's no longer used 2024-04-24 18:56:09 -06:00
79b14355d9 add choice of a start date for a project 2024-04-23 20:48:38 -06:00
c5102f89b5 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
ab6d2e3d4b remove message confusion, add project name during give-details 2024-04-21 20:31:57 -06:00
d1a285d659 change the "give" action on contact page to use dialog box 2024-04-21 16:42:22 -06:00
bba183dc46 add 'offer' on contact screen 2024-04-21 07:38:59 -06:00
676882978a add code to display profiles in feed, but deactivate it for now 2024-04-20 19:53:11 -06:00
dc9720560e increment version and add "-beta" 2024-04-20 08:14:53 -06:00
15c026c80c bump to v 0.3.8 2024-04-20 08:06:34 -06:00
03f722f38a make so cropping isn't behind header; delete profile image from storage when deleted 2024-04-19 20:13:44 -06:00
f14c3de0ef Merge pull request 'profile-pic' (#114) from profile-pic into master
Reviewed-on: #114
2024-04-19 17:36:53 -04:00
606f21faec make the home screen elements load more quickly 2024-04-19 15:37:10 -06:00
5a9958cb4f show contact's or user's icon in more places 2024-04-19 11:39:01 -06:00
b11cf81bf9 crop the image and store online and in settings 2024-04-18 20:27:43 -06:00
734e28667d add photo to profile page (not yet saved) 2024-04-17 20:07:09 -06:00
405bc22dae fix contact sorting to show those without names 2024-04-17 19:29:17 -06:00
1758cbee98 update ClickUp link to a public link 2024-04-17 11:05:34 -06:00
5359b241f7 remove tasks here in favor of ClickUp 2024-04-16 20:13:04 -06:00
555ac34d18 note that tasks have moved 2024-04-11 20:43:52 -06:00
acbbdf0e8b bump version and add "-beta" 2024-04-10 19:40:16 -06:00
cf18f1543a bump to v 0.3.7 2024-04-10 19:32:46 -06:00
df829778da open the app when notification is clicked 2024-04-10 19:31:14 -06:00
7ae431a9e7 fix PWA creation & service-worker registration, plus some commentary tweaks 2024-04-09 20:29:21 -06:00
77becf8673 remove non-working interests, enhance error messages, update tasks & changelog 2024-04-09 17:54:17 -06:00
a0ef8b6fd3 Merge pull request 'vitejs refactor' (#110) from jsnbuchanan/crowd-funder-for-time-pwa:feat/vitejs into master
Reviewed-on: #110
2024-04-09 19:49:48 -04:00
1befff0abd Merge pull request 'misc tweaks for new vite build' (#4) from trentlarson/crowd-funder-from-jason:feat/vitejs-trent3 into feat/vitejs
Reviewed-on: jsnbuchanan/crowd-funder-for-time-pwa#4
2024-04-09 06:05:55 -04:00
0b446ec134 misc tweaks for new vite build 2024-04-07 18:12:33 -06:00
333ac773f6 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: jsnbuchanan/crowd-funder-for-time-pwa#1
2024-04-07 13:52:43 -04:00
1459719a47 remove a lingering debug console.log 2024-04-07 11:39:13 -06:00
5994365a6c fix title of the test app 2024-04-07 11:32:53 -06:00
28f72640d7 add linting before any build 2024-04-07 11:22:20 -06:00
ab81648aca fix linting 2024-04-07 11:02:54 -06:00
5828a290c7 Merge remote-tracking branch 'original-origin/master' into feat/vitejs-trent 2024-04-07 09:41:14 -06:00
78fab735e6 avoid a huge error message in a likely-well-known scenario 2024-04-07 09:24:55 -06:00
2ae165d56f reorder home page vapid check to avoid an error on localhost 2024-04-07 09:16:42 -06:00
0fbd1ad51a add missing Dexie import (which causes failure upon download click) 2024-04-07 09:13:32 -06:00
d49bf61524 on home page, change the filtered button color 2024-04-06 17:58:10 -06:00
1a80bbb714 Merge pull request 'ui-additions-2024-03' (#113) from ui-additions-2024-03 into master
Reviewed-on: #113
2024-04-06 19:46:16 -04:00
5a2a8659f7 Merge branch 'master' into ui-additions-2024-03 2024-04-06 17:45:32 -06:00
0632fb9b39 show in description when recipient is a project (not just Anonymous) 2024-04-06 17:39:40 -06:00
640d273646 filter by selections (now all working), add cache for plans 2024-04-06 14:01:18 -06:00
8f4289c14d Merge pull request 'send a time for notifications to the push server' (#112) from notify-time into master
Reviewed-on: #112
2024-04-03 22:04:36 -04:00
7f56c90d97 Merge pull request 'ui-fixes-2024-03' (#111) from ui-fixes-2024-03 into master
Reviewed-on: #111
2024-04-03 22:04:02 -04:00
8e1daf7015 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
e90a0be6d9 Names and variables for filter toggles 2024-04-03 15:51:23 +08:00
489bb76a60 adjust more code to the PushSubscriptionJSON 2024-04-02 19:32:41 -06:00
2ca33bb9eb adjust the notification-subscription objects to try and send correct info 2024-04-02 19:18:31 -06:00
121181c6a1 add adjustment to UTC hour for notification time 2024-04-02 18:38:44 -06:00
e4cf79b558 update tasks 2024-04-01 19:06:18 -06:00
7692cc2b35 add logic to send a time for notifications 2024-04-01 19:04:54 -06:00
Jose Olarte III
0b4f2484f7 Additions to Account View 2024-03-29 21:41:14 +08:00
Jose Olarte III
cfd53bc186 Removed one more 2024-03-29 15:55:16 +08:00
Jose Olarte III
7f66addfe3 Filter options reduced for release 2024-03-29 15:53:46 +08:00
Jose Olarte III
4635c1ac48 Feed filters dialog 2024-03-27 19:57:31 +08:00
Jose Olarte III
55da3d0b1c Map fix #2 2024-03-26 21:38:21 +08:00
Jose Olarte III
dce4d3cc72 Button width changes
For buttons that are next to each other
2024-03-26 19:55:16 +08:00
Jose Olarte III
e028197a2a Optimized grid space for wider screens 2024-03-26 17:12:55 +08:00
Jose Olarte III
0dab475d8b Fixed map z-index 2024-03-26 16:54:43 +08:00
Jose Olarte III
4e227fc07a Added close icon to gifted prompts dialog 2024-03-26 16:02:24 +08:00
2dfc8fedaa refactor tasks 2024-03-25 19:03:01 -06:00
035f2a5b04 docs: adding do for updated development server run command
- `npm run dev`
2024-03-25 08:15:04 -06:00
09dccc34d6 fix: buffer typescript error in util.ts when parsing ArrayBuffer 2024-03-25 08:10:38 -06:00
b28104af5b bump version and add -beta 2024-03-24 19:04:24 -06:00
3a07e31d63 bump to version 0.3.6 2024-03-24 18:28:42 -06:00
35455e6648 fix check for more camera-device options 2024-03-24 18:27:06 -06:00
0e2c5af16e add onboarding help instructions as separate page 2024-03-24 17:01:53 -06:00
1fc5b0ea2b Merge pull request 'add button during photo to switch to mirror mode' (#109) from photo-reverse into master
Reviewed-on: #109
2024-03-24 18:59:50 -04:00
ca240ab795 fix: es modules syntax for buffer deps instead of commonjs require 2024-03-24 13:05:22 -06:00
01b5ca6ec8 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
6f49260c1e 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
38f44771e9 Initial stab at vitejs update 2024-03-24 11:18:29 -06:00
be2154f847 change icon for detail view (from circle-info to file-lines) 2024-03-23 19:07:53 -06:00
4ad4cab25e add blurb explaining what data is shared with the world 2024-03-23 18:45:26 -06:00
e020caaa50 show warnings before dismissing prompt, and add to tasks and help 2024-03-23 17:35:58 -06:00
40d12b1f9c add button on photo to switch to mirror mode 2024-03-23 16:31:23 -06:00
28754bdfb1 bump version to 0.3.5 2024-03-23 02:41:25 -06:00
2b8f9579f1 fix so that project agent & location removals get saved 2024-03-23 02:31:44 -06:00
6dc0c2cd58 add a camera-switch button 2024-03-23 01:32:55 -06:00
cc6d0958dc 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: #108
2024-03-22 12:01:21 -04:00
7ce00b86e8 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
85 changed files with 2170 additions and 7269 deletions

View File

@@ -1,6 +1,4 @@
# Only the variables that start with VITE_ are seen in the application import.meta.env in Vue.
VITE_APP_SERVER=https://timesafari.app
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
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.timesafari.app

View File

@@ -14,21 +14,8 @@ module.exports = {
// ecmaVersion: 2020,
// },
rules: {
"max-len": [
"warn",
{
code: 120,
ignoreComments: true, // why does this not make it allow comment of any length?
ignorePattern: '^\\s*class="[^"]*"$',
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreTrailingComments: true,
ignoreUrls: true,
},
],
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
// "prettier/prettier": ["warn", { printWidth: 120 }], // removes errors but adds thousands of warnings
"@typescript-eslint/no-unnecessary-type-constraint": "off",
},
};

View File

@@ -6,87 +6,9 @@ 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).
## [0.3.35] - 2024.11.24
## [0.3.21] - 2024.08.24
### Added
- Daily reliable, hard-coded notification message
- Setting to change the partner API server
## [0.3.33] - 2024.11.07 - adb7b16ecf1343c39cba71a7d6bb0e7a973e1102
### Fixed
- Affirm Delivery button on offer claim page didn't work.
- Plans were not showing by default on project page.
## [0.3.32] - 2024.11.06 - 9a3fa38a3fd28f977e06f0265fc39e635c9c5ccd
### Added
- Highlight new offers to user & to user's projects on the front page.
## [0.3.31] - 2024.10.25 - 07c02ab98a09d293dd90d9289a7872e7d681d296
### Changed
- Onboarding messages about offers
## [0.3.30]
### Added
- Onboarding messages
## [0.3.29] - 2024.10.09 - babd3832bdfe0c40eaa3869de1b41399a51713c1
### Added
- Invite for a contact to join immediately
### Changed
- Send signed data to nostr endpoints to verify public key ownership.
- Enhanced help & help onboarding.
### Changed in DB or environment
- Uses Endorser.ch version 4.1.1
## [0.3.28] - 2024.09.30 - 84720b94049d29cc0ddd99c50cef2e7176130133
### Added
- Posting to nostr apps Trustroots & TripHopping
- Display of providers on claim view page
### Changed
- Switched BVC-meeting-ending gift to be a gift from the group.
### Changed in DB or environment
- Requires Endorser.ch version 4.1.0
## [0.3.27] - 2024.09.22 - ee23e6f005e47f5bd6f04d804599f6395371b0e4
### Fixed
- Error loading BVC claims to confirm
- Really allow visibility of bulk-imported contacts
## [0.3.26] - 2024.09.16 - 8263ed2b29947b3ccc6f3133bbc9454c222bce28
### Added
- Separate 'isRegistered' flag for each account
### Fixed
- Failure to assign offers to their project
- Alert when looking at one's own activity if not in contacts.
## [0.3.25] - 2024.08.30 - dcbe02d877aecb4cdef2643d90e6595d246a9f82
### Added
- "Ideas" now jumps directly to giving prompt or contact list.
### Fixed
- Empty giver name on gifted-details view
- Previously visited project would show up on the giving-details page.
### Removed
- All unnecessary localStorage for project IDs
## [0.3.23] - 2024.08.30
### Added
- Sections in Help for different kinds of users
- Discovery page parameters so that links with search text work
- Message when no projects are found
## [0.3.21] - 2024.08.24 - a7b89f4bb6da928d56daeffaae7741fa74cc80bf
### Added
- Send list of contacts to someone, and move individual contact actions to detail page.
- Send list of contacts to someone.
- Prompt for name in pop-up, and send to different contact-sharing screens.
### Changed
- Moved contact actions from list onto detail page

View File

@@ -21,8 +21,6 @@ npm install
npm run dev
```
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
### Build the test & production app
```
npm run serve
@@ -33,14 +31,6 @@ npm run serve
npm run lint
```
### Run all important tests
... including automated UI tests (see below for details)
```
npm run test-all
```
### 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.
@@ -51,9 +41,7 @@ npm run test-all
* Commit everything (since the commit hash is used the app).
* Put the commit hash in the changelog (which will help you remember to bump the version later).
* Record what version is currently on production in docs.
* Record what version is currently on production.
* Run the correct build:
@@ -61,7 +49,7 @@ npm run test-all
```
# (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_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
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
@@ -74,7 +62,7 @@ npm run build
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
* 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.
* 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)
@@ -109,7 +97,7 @@ It's possible to use the global test Endorser (ledger) server (but currently the
It's possible to run with a minimal set of data; the following starts with the bare minimum of test data:
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
@@ -118,13 +106,6 @@ NODE_ENV=test-local npm run dev
```
To run a single test like above with the screenshots, use the following:
```
npx playwright test -c playwright.config-local.ts --trace on test-playwright/40-add-contact.spec.ts
```
### Register new user on test server
On the test server, User #0 has rights to register others, so you can start

2221
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "TimeSafari",
"version": "0.3.35",
"version": "0.3.21",
"scripts": {
"dev": "vite",
"serve": "vite preview",
@@ -12,10 +12,6 @@
"test-all": "npm run build && npx playwright test -c playwright.config-local.ts --trace on"
},
"dependencies": {
"@capacitor/android": "^6.1.2",
"@capacitor/cli": "^6.1.2",
"@capacitor/core": "^6.1.2",
"@capacitor/ios": "^6.1.2",
"@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1",
"@ethersproject/hdnode": "^5.7.0",
@@ -45,7 +41,6 @@
"dexie": "^3.2.7",
"dexie-export-import": "^4.1.1",
"did-jwt": "^7.4.7",
"did-resolver": "^4.1.0",
"ethereum-cryptography": "^2.1.3",
"ethereumjs-util": "^7.1.5",
"jdenticon": "^3.2.0",
@@ -55,7 +50,6 @@
"lru-cache": "^10.2.0",
"luxon": "^3.4.4",
"merkletreejs": "^0.3.11",
"nostr-tools": "^2.7.2",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",

View File

@@ -1,4 +1,4 @@
import { defineConfig, devices } from "@playwright/test";
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
@@ -11,7 +11,7 @@ import { defineConfig, devices } from "@playwright/test";
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./test-playwright",
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. */
@@ -21,44 +21,44 @@ export default defineConfig({
/* 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",
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",
baseURL: 'http://localhost:8080',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
name: 'chromium',
use: {
...devices["Desktop Chrome"],
...devices['Desktop Chrome'],
permissions: ["clipboard-read"],
},
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
{
name: "Mobile Chrome",
use: { ...devices["Pixel 5"] },
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: "Mobile Safari",
use: { ...devices["iPhone 12"] },
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
/* Test against branded browsers. */
@@ -67,14 +67,14 @@ export default defineConfig({
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
{
name: "Google Chrome",
use: { ...devices["Desktop Chrome"], channel: "chrome" },
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: 10000,
// timeout: 5000,
/* Run your local dev server before starting the tests */
/**
@@ -91,7 +91,7 @@ export default defineConfig({
*/
webServer: {
command:
"VITE_APP_SERVER=http://localhost:8080 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_PASSKEYS_ENABLED=true npm run dev",
"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,86 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2480 4005 c-25 -7 -58 -20 -75 -29 -16 -9 -40 -16 -52 -16 -17 0
-24 -7 -28 -27 -3 -16 -14 -45 -24 -65 -21 -41 -13 -55 18 -38 25 13 67 13 92
-1 15 -8 35 -4 87 17 99 39 130 41 197 10 64 -29 77 -31 107 -15 20 11 20 11
-3 35 -12 13 -30 24 -38 24 -24 1 -132 38 -148 51 -8 7 -11 20 -7 32 12 37
-40 47 -126 22z"/>
<path d="M1450 3775 c-7 -8 -18 -15 -24 -15 -7 0 -31 -14 -54 -32 -29 -22 -38
-34 -29 -40 17 -11 77 -10 77 1 0 5 16 16 35 25 60 29 220 19 290 -18 17 -9
33 -16 37 -16 4 0 31 -15 60 -34 108 -70 224 -215 282 -353 30 -71 53 -190 42
-218 -10 -27 -23 -8 -52 75 -30 90 -88 188 -120 202 -13 6 -26 9 -29 6 -3 -2
11 -51 30 -108 28 -83 35 -119 35 -179 0 -120 -22 -127 -54 -17 -11 37 -13 21
-18 -154 -5 -180 -8 -200 -32 -264 -51 -132 -129 -245 -199 -288 -21 -12 -79
-49 -129 -80 -161 -102 -294 -141 -473 -141 -228 0 -384 76 -535 259 -81 99
-118 174 -154 312 -31 121 -35 273 -11 437 19 127 19 125 -4 125 -23 0 -51
-34 -87 -104 -14 -28 -33 -64 -41 -81 -19 -34 -22 -253 -7 -445 9 -106 12
-119 44 -170 19 -30 42 -67 50 -81 64 -113 85 -140 130 -169 28 -18 53 -44 61
-62 8 -20 36 -45 83 -76 62 -39 80 -46 151 -54 44 -5 96 -13 115 -18 78 -20
238 -31 282 -19 24 6 66 8 95 5 76 -9 169 24 319 114 32 19 80 56 106 82 27
26 52 48 58 48 5 0 27 26 50 58 48 66 56 70 132 71 62 1 165 29 238 64 112 55
177 121 239 245 37 76 39 113 10 267 -12 61 -23 131 -26 156 -5 46 -5 47 46
87 92 73 182 70 263 -8 l51 -49 -6 -61 c-4 -34 -13 -85 -21 -113 -28 -103 -30
-161 -4 -228 16 -44 32 -67 55 -83 18 -11 39 -37 47 -58 10 -23 37 -53 73 -81
32 -25 69 -57 82 -71 14 -14 34 -26 47 -26 12 0 37 -7 56 -15 20 -8 66 -17
104 -20 107 -10 110 -11 150 -71 50 -75 157 -177 197 -187 18 -5 53 -24 78
-42 71 -51 176 -82 304 -89 61 -4 127 -12 147 -18 29 -9 45 -8 77 6 23 9 50
16 60 16 31 0 163 46 216 76 28 15 75 46 105 69 30 23 69 49 85 58 17 8 46 31
64 51 19 20 40 36 47 36 18 0 77 70 100 120 32 66 45 108 55 173 5 32 16 71
24 87 43 84 43 376 0 549 -27 105 -43 127 -135 188 -30 21 -65 46 -77 57 -13
11 -23 17 -23 14 0 -3 21 -46 47 -94 79 -151 85 -166 115 -263 25 -83 28 -110
28 -226 0 -144 -17 -221 -75 -335 -39 -77 -208 -244 -304 -299 -451 -263 -975
-67 -1138 426 -23 70 -26 95 -28 254 -1 108 -7 183 -14 196 -6 12 -11 31 -11
43 0 32 31 122 52 149 10 13 18 28 18 34 0 5 25 40 56 78 60 73 172 170 219
190 30 12 30 13 6 17 -15 2 -29 -2 -37 -12 -6 -9 -16 -16 -22 -16 -6 0 -23
-11 -39 -24 -15 -12 -33 -25 -40 -27 -17 -6 -82 -60 -117 -97 -65 -70 -75 -82
-107 -133 -23 -34 -35 -46 -37 -35 -3 16 20 87 44 134 6 12 9 34 6 48 -4 22
-8 25 -31 19 -14 -3 -38 -15 -53 -26 -34 -24 -34 -21 -6 28 65 112 184 206
291 227 15 3 39 9 55 12 l27 6 -24 9 c-90 35 -304 -66 -478 -225 -39 -36 -74
-66 -77 -66 -22 0 18 82 72 148 19 23 32 46 28 49 -4 4 -26 13 -49 19 -73 21
-161 54 -171 64 -6 6 -20 10 -32 10 -21 0 -21 -1 -8 -40 45 -130 8 -247 -93
-299 -25 -13 -31 0 -14 29 15 22 1 33 -22 17 -56 -36 -117 -22 -117 28 0 13
-16 47 -35 76 -22 34 -33 60 -29 73 4 16 -3 26 -26 39 -16 10 -30 21 -30 25 1
18 54 64 87 76 l38 13 -33 5 c-30 4 -115 -18 -154 -42 -13 -7 -20 -5 -27 8 -9
16 -12 16 -53 1 -160 -61 -258 -104 -258 -114 0 -7 10 -20 21 -31 103 -91 217
-297 249 -449 28 -135 41 -237 35 -276 -14 -91 -48 -170 -97 -220 -44 -47 -68
-60 -68 -40 0 6 4 12 8 15 5 3 24 35 42 72 l33 67 -6 141 c-4 103 -11 158 -26
205 -12 35 -21 70 -21 77 0 7 -20 56 -45 108 -82 173 -227 322 -392 401 -67
33 -90 39 -163 42 -108 5 -130 10 -130 28 0 20 -63 20 -80 0z"/>
<path d="M3710 3765 c0 -20 8 -28 39 -41 22 -8 42 -22 45 -30 5 -14 42 -19 70
-8 10 4 -7 21 -58 55 -41 27 -79 49 -85 49 -6 0 -11 -11 -11 -25z"/>
<path d="M3173 3734 c-9 -25 10 -36 35 -18 12 8 22 19 22 25 0 16 -50 10 -57
-7z"/>
<path d="M1982 3728 c6 -16 36 -34 44 -26 3 4 4 14 1 23 -7 17 -51 21 -45 3z"/>
<path d="M1540 3620 c0 -5 7 -10 16 -10 8 0 12 5 9 10 -3 6 -10 10 -16 10 -5
0 -9 -4 -9 -10z"/>
<path d="M4467 3624 c-4 -4 23 -27 60 -50 84 -56 99 -58 67 -9 -28 43 -107 79
-127 59z"/>
<path d="M655 3552 c-11 -2 -26 -9 -33 -14 -7 -6 -27 -18 -45 -27 -36 -18 -58
-64 -39 -83 9 -9 25 1 70 43 53 48 78 78 70 84 -2 1 -12 -1 -23 -3z"/>
<path d="M1015 3460 c-112 -24 -247 -98 -303 -165 -53 -65 -118 -214 -136
-311 -20 -113 -20 -145 -1 -231 20 -88 49 -153 102 -230 79 -113 186 -182 331
-214 108 -24 141 -24 247 1 130 30 202 72 316 181 102 100 153 227 152 384 0
142 -58 293 -150 395 -60 67 -180 145 -261 171 -75 23 -232 34 -297 19z m340
-214 c91 -43 174 -154 175 -234 0 -18 -9 -51 -21 -73 -19 -37 -19 -42 -5 -64
35 -54 12 -121 -48 -142 -22 -7 -47 -19 -55 -27 -9 -8 -41 -27 -71 -42 -50
-26 -64 -29 -155 -29 -111 0 -152 14 -206 68 -49 49 -63 85 -64 162 0 59 4 78
28 118 31 52 96 105 141 114 23 5 33 17 56 68 46 103 121 130 225 81z"/>
<path d="M3985 3464 c-44 -7 -154 -44 -200 -67 -55 -28 -138 -96 -162 -132
-10 -16 -39 -75 -64 -130 l-44 -100 0 -160 0 -160 45 -90 c53 -108 152 -214
245 -264 59 -31 215 -71 281 -71 53 0 206 40 255 67 98 53 203 161 247 253 53
113 74 193 74 280 -1 304 -253 564 -557 575 -49 2 -103 1 -120 -1z m311 -220
c129 -68 202 -209 160 -309 -15 -35 -15 -42 -1 -72 26 -55 -3 -118 -59 -129
-19 -3 -43 -15 -53 -26 -26 -29 -99 -64 -165 -78 -45 -10 -69 -10 -120 -1 -74
15 -113 37 -161 91 -110 120 -50 331 109 385 24 8 44 23 52 39 6 14 18 38 25
53 33 72 127 93 213 47z"/>
<path d="M487 3394 c-21 -12 -27 -21 -25 -40 2 -14 7 -26 12 -27 14 -3 48 48
44 66 -3 14 -6 14 -31 1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -229,7 +229,7 @@
? notification.onCancel(stopAsking)
: null;
close(notification.id);
stopAsking = false; // reset value for next time they open this modal
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"
>
@@ -238,7 +238,63 @@
</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"
>
<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 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"
@@ -252,17 +308,17 @@
<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 1 Day
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 2 Days
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 1 Week
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"
@@ -278,7 +334,6 @@
</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"
@@ -288,17 +343,17 @@
>
<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> this notification?
Would you like to <b>turn off</b> notifications for this app?
</p>
<button
@click="
close(notification.id);
turnOffNotifications(notification);
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 Notification
Turn Off Notifications
</button>
<button
@click="close(notification.id)"
@@ -318,108 +373,420 @@
<style></style>
<script lang="ts">
import axios from "axios";
import { Vue, Component } from "vue-facing-decorator";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "@/db/index";
import { NotificationIface } from "./constants/app";
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 turnOffNotifications(notification: NotificationIface) {
let subscription: object | null = null;
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;
}
let allGoingOff = false;
const settings = await retrieveSettingsForActiveAccount();
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
const notifyingReminder = !!settings?.notifyingReminderTime;
if (!notifyingNewActivity || !notifyingReminder) {
// the other notification is already off, so fully unsubscribe now
allGoingOff = true;
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.");
}
await navigator.serviceWorker?.ready
.then((registration) => {
return registration.pushManager.getSubscription();
})
.then(async (subscript: PushSubscription | null) => {
if (subscript) {
subscription = subscript.toJSON();
if (allGoingOff) {
await subscript.unsubscribe();
}
} else {
logConsoleAndDb("Subscription object is not available.");
}
})
.catch((error) => {
logConsoleAndDb(
"Push provider server communication failed: " + JSON.stringify(error),
true,
);
});
const secret = localStorage.getItem("secret");
if (!secret) {
return Promise.reject("No secret found.");
}
if (!subscription) {
// there is no endpoint or auth for the server to compare, so we're done
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: "info",
title: "Finished",
text: "Notifications are off.", // a different message so I know there are none stored
type: "danger",
title: "Not a Number",
text: "The time must be an hour number.",
},
5000,
);
return true;
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;
}
const serverSubscription = {
...subscription,
};
if (!allGoingOff) {
serverSubscription["notifyType"] = notification.title;
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(serverSubscription),
body: JSON.stringify(subscription),
})
.then((response) => {
return response.ok;
})
.catch((error) => {
logConsoleAndDb(
"Push server communication failed: " + JSON.stringify(error),
true,
);
console.error("Push server communication failed:", error);
return false;
});
let message;
if (pushServerSuccess) {
message = "Notification is off.";
} else {
message = "Notification is still on. Try to turn it off again.";
}
this.$notify(
{
group: "alert",
type: "info",
title: "Finished",
text: message,
},
5000,
alert(
"Notifications are off. Push provider unsubscribe " +
(pushProviderSuccess ? "succeeded" : "failed") +
(pushProviderSuccess === pushServerSuccess ? " and" : " but") +
" push server unsubscribe " +
(pushServerSuccess ? "succeeded" : "failed") +
".",
);
if (notification.callback) {
// it's OK if the local notifications are still on (especially if the other notification is on)
notification.callback(pushServerSuccess);
}
}
}
</script>

View File

@@ -1,99 +0,0 @@
<!-- similar to UserNameDialog -->
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">{{ title }}</h1>
{{ message }}
Note that their name is only stored on this device.
<input
type="text"
placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="newText"
/>
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
type="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 mb-2"
@click="onClickSaveChanges()"
>
Save
</button>
<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-2 py-3 rounded-md mb-2"
@click="onClickCancel()"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
@Component
export default class ContactNameDialog extends Vue {
cancelCallback: () => void = () => {};
saveCallback: (name?: string) => void = () => {};
message = "";
newText = "";
title = "Contact Name";
visible = false;
async open(
title?: string,
message?: string,
saveCallback?: (name?: string) => void,
cancelCallback?: () => void,
) {
this.cancelCallback = cancelCallback || this.cancelCallback;
this.saveCallback = saveCallback || this.saveCallback;
this.message = message ?? this.message;
this.title = title ?? this.title;
this.visible = true;
}
async onClickSaveChanges() {
this.visible = false;
if (this.saveCallback) {
this.saveCallback(this.newText);
}
}
onClickCancel() {
this.visible = false;
if (this.cancelCallback) {
this.cancelCallback();
}
}
}
</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

@@ -100,7 +100,7 @@ import {
} from "@vue-leaflet/vue-leaflet";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { db } from "@/db/index";
@Component({
components: {
@@ -121,10 +121,11 @@ export default class FeedFilters extends Vue {
async open(onCloseIfChanged: () => void) {
this.onCloseIfChanged = onCloseIfChanged;
const settings = await retrieveSettingsForActiveAccount();
this.hasVisibleDid = !!settings.filterFeedByVisible;
this.isNearby = !!settings.filterFeedByNearby;
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
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;
}

View File

@@ -7,7 +7,7 @@
<input
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
:placeholder="prompt || 'What was given?'"
placeholder="What was given"
v-model="description"
/>
<div class="flex flex-row justify-center">
@@ -47,7 +47,7 @@
giverDid: giver?.did,
giverName: giver?.name,
offerId,
fulfillsProjectId: projectId,
projectId,
recipientDid: receiver?.did,
recipientName: receiver?.name,
unitCode,
@@ -89,9 +89,14 @@
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
import {
createAndSubmitGive,
didInfo,
GiverReceiverInputInfo,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Contact } from "@/db/tables/contacts";
@Component
@@ -109,27 +114,25 @@ export default class GiftedDialog extends Vue {
callbackOnSuccess?: (amount: number) => void = () => {};
customTitle?: string;
description = "";
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
giver?: GiverReceiverInputInfo; // undefined means no identified giver agent
isTrade = false;
offerId = "";
prompt = "";
receiver?: libsUtil.GiverReceiverInputInfo;
receiver?: GiverReceiverInputInfo;
unitCode = "HUR";
visible = false;
libsUtil = libsUtil;
async open(
giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo,
giver?: GiverReceiverInputInfo,
receiver?: GiverReceiverInputInfo,
offerId?: string,
customTitle?: string,
prompt?: string,
callbackOnSuccess?: (amount: number) => void,
) {
this.customTitle = customTitle;
this.description = "";
this.giver = giver;
this.prompt = prompt || "";
this.receiver = receiver;
// if we show "given to user" selection, default checkbox to true
this.amountInput = "0";
@@ -137,9 +140,10 @@ export default class GiftedDialog extends Vue {
this.offerId = offerId || "";
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
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();
@@ -203,7 +207,6 @@ export default class GiftedDialog extends Vue {
this.description = "";
this.giver = undefined;
this.amountInput = "0";
this.prompt = "";
this.unitCode = "HUR";
}

View File

@@ -19,12 +19,12 @@
</span>
<div class="m-2">
<span v-if="currentCategory === CATEGORY_IDEAS">
<span v-if="currentIdeaIndex < IDEAS.length">
<p class="text-center text-lg font-bold">
{{ IDEAS[currentIdeaIndex] }}
</p>
</span>
<div v-if="currentCategory === CATEGORY_CONTACTS">
<div v-if="currentIdeaIndex == IDEAS.length + 0">
<p class="text-center">
<span
v-if="currentContact == null"
@@ -61,7 +61,7 @@
</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="proceed"
@click="cancel"
>
That's it!
</button>
@@ -71,168 +71,150 @@
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { GiverReceiverInputInfo } from "@/libs/util";
@Component
export default class GivenPrompts extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
CATEGORY_CONTACTS = 1;
CATEGORY_IDEAS = 0;
IDEAS = [
"What food did someone fix for you?",
"What did a family member do for you?",
"What compliment did someone give you?",
"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?",
"What did you see someone give to someone else?",
"What is a way that someone helped you even though you have never met?",
"How did a musician or author or artist inspire you?",
"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?",
"What is something worth respect that an organization gave you?",
"Did some organization give something worth respect?",
"Who last gave you a good laugh?",
"What do you recall someone giving you while you were young?",
"Who forgave you or overlooked a mistake?",
"What is a way an ancestor contributed to your life?",
"What kind of help did someone at work give you?",
"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
callbackOnFullGiftInfo?: (
contactInfo?: GiverReceiverInputInfo,
description?: string,
) => void;
currentCategory = this.CATEGORY_IDEAS; // 0 = IDEAS, 1 = CONTACTS
currentContact: Contact | undefined = undefined;
currentIdeaIndex = 0;
numContacts = 0;
shownContactDbIndices: Array<boolean> = [];
shownContactDbIndices: number[] = [];
visible = false;
AppString = AppString;
async open(
callbackOnFullGiftInfo: (
contactInfo: GiverReceiverInputInfo,
description: string,
) => void,
) {
async open() {
this.visible = true;
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
await db.open();
this.numContacts = await db.contacts.count();
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
}
cancel() {
this.currentCategory = this.CATEGORY_IDEAS;
this.currentContact = undefined;
this.currentIdeaIndex = 0;
this.numContacts = 0;
this.shownContactDbIndices = [];
close() {
// close the dialog but don't change values (just in case some actions are added later)
this.visible = false;
}
proceed() {
// proceed with logic but don't change values (just in case some actions are added later)
this.visible = false;
if (this.currentCategory === this.CATEGORY_IDEAS) {
(this.$router as Router).push({
name: "contact-gift",
query: {
prompt: this.IDEAS[this.currentIdeaIndex],
},
});
} else {
// must be this.CATEGORY_CONTACTS
this.callbackOnFullGiftInfo?.(
this.currentContact as GiverReceiverInputInfo,
);
}
}
/**
* Get the next idea.
* If it is a contact prompt, loop through.
*/
async nextIdea() {
// check if the next one is an idea or a contact
if (this.currentCategory === this.CATEGORY_IDEAS) {
this.currentIdeaIndex++;
if (this.currentIdeaIndex === this.IDEAS.length) {
// must have just finished ideas so move to contacts
this.findNextUnshownContact();
}
} else {
// must be this.CATEGORY_CONTACTS
// 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();
// when that's finished, it'll reset to ideas
} 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 = [];
}
}
/**
* Get the previous idea.
* If it is a contact prompt, loop through.
*/
async prevIdea() {
// check if the next one is an idea or a contact
if (this.currentCategory === this.CATEGORY_IDEAS) {
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) {
// must have just finished ideas so move to contacts
this.findNextUnshownContact();
this.currentIdeaIndex = this.IDEAS.length - 1 + this.OTHER_PROMPTS;
}
} else {
// must be this.CATEGORY_CONTACTS
this.findNextUnshownContact();
// when that's finished, it'll reset to ideas
// ... and clear out any other prompt info
this.currentContact = undefined;
this.shownContactDbIndices = [];
}
}
nextIdeaPastContacts() {
this.currentIdeaIndex = 0;
this.currentContact = undefined;
this.shownContactDbIndices = new Array<boolean>(this.numContacts);
this.currentCategory = this.CATEGORY_IDEAS;
// look at the previous idea and switch to the other side of the list
this.currentIdeaIndex =
this.currentIdeaIndex >= this.IDEAS.length ? 0 : this.IDEAS.length - 1;
this.shownContactDbIndices = [];
}
async findNextUnshownContact() {
if (this.currentCategory === this.CATEGORY_IDEAS) {
// we're not in the contact prompts, so reset index array
this.shownContactDbIndices = new Array<boolean>(this.numContacts);
}
this.currentCategory = this.CATEGORY_CONTACTS;
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
let count = 0;
// as long as the index has an entry, loop
while (
this.shownContactDbIndices[someContactDbIndex] != null &&
count++ < this.numContacts
) {
someContactDbIndex = (someContactDbIndex + 1) % this.numContacts;
}
if (count >= this.numContacts) {
// all contacts have been shown
this.nextIdeaPastContacts();
// 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();
this.shownContactDbIndices[someContactDbIndex] = true;
}
}
cancel() {
this.currentContact = undefined;
this.currentIdeaIndex = 0;
this.numContacts = 0;
this.shownContactDbIndices = [];
this.close();
}
}
</script>

View File

@@ -1,118 +0,0 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Invitation & Notes</h1>
These are optional notes for your use; they are comments to help you
recall who it is when they accept it. These notes are sent to the server.
If you want to store your own way, the invitation ID is:
{{ inviteIdentifier }}
<input
type="text"
placeholder="Notes"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="text"
/>
<!-- Add date selection element -->
Expiration
<input
type="date"
class="block rounded border border-slate-400 mb-4 px-3 py-2"
v-model="expiresAt"
/>
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
type="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 mb-2"
@click="onClickSaveChanges()"
>
Save
</button>
<!-- SHOW ME instead while processing saving changes -->
<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-2 py-3 rounded-md mb-2"
@click="onClickCancel()"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
@Component
export default class InviteDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
callback: (text: string, expiresAt: string) => void = () => {};
inviteIdentifier = "";
text = "";
visible = false;
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)
.toISOString()
.substring(0, 10);
async open(
inviteIdentifier: string,
aCallback: (text: string, expiresAt: string) => void,
) {
this.callback = aCallback;
this.inviteIdentifier = inviteIdentifier;
this.visible = true;
}
async onClickSaveChanges() {
if (!this.expiresAt) {
this.$notify(
{
group: "alert",
type: "warning",
title: "Needs Expiration",
text: "You must select an expiration date.",
},
5000,
);
} else {
this.callback(this.text, this.expiresAt);
this.visible = false;
}
}
onClickCancel() {
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: 500px;
}
</style>

View File

@@ -6,7 +6,7 @@
type="text"
data-testId="inputDescription"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Description of what is offered"
placeholder="Description, prerequisites, terms, etc."
v-model="description"
/>
<div class="flex flex-row mt-2">
@@ -85,14 +85,15 @@ 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 { retrieveSettingsForActiveAccount } from "@/db/index";
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?: string;
@Prop projectName?: string;
@Prop projectId?;
@Prop projectName?;
activeDid = "";
apiServer = "";
@@ -112,9 +113,10 @@ export default class OfferDialog extends Vue {
this.recipientDid = recipientDid;
this.recipientName = recipientName;
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
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) {
@@ -207,9 +209,9 @@ export default class OfferDialog extends Vue {
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record an offer.",
text: "You must select an identifier before you can record an offer.",
},
7000,
-1,
);
return;
}
@@ -235,7 +237,6 @@ export default class OfferDialog extends Vue {
description,
amount,
unitCode,
"",
expirationDateInput,
this.recipientDid,
this.projectId,
@@ -264,7 +265,7 @@ export default class OfferDialog extends Vue {
title: "Success",
text: "That offer was recorded.",
},
5000,
10000,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,281 +0,0 @@
<!-- similar to ContactNameDialog -->
<template>
<div v-if="visible" class="dialog-overlay">
<div v-if="page === OnboardPage.Home" class="dialog">
<h1 class="text-xl font-bold text-center mb-4 relative">
Welcome to Time Safari
<br />
- Showcasing Gratitude & Magnifing Time
<div
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</h1>
<p v-if="isRegistered" class="mt-4">
You can now log things that you've received or witnessed:
<span v-if="numContacts > 0">
click on {{ firstContactName }}'s name or
</span>
click on "Unnamed" to express your appreciation for... whatever -- like
thanks for showing you all these fascinating stories of
<em>gratitude</em>.
</p>
<p v-else class="mt-4">
The feed underneath this pop-up shows the latest gifts recognized by
others. Once someone registers you, you'll be able to log your
appreciation, too.
</p>
<p class="mt-4">
The more you illuminate cool things people are doing, the more you
attract people to work together with you.
</p>
<p class="mt-4 flex items-center">
The
<fa
icon="house-chimney"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
button below brings you back to this feed screen.
</p>
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
type="button"
data-testId="closeOnboardingAndFinish"
class="block w-full text-center text-md 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-2 py-3 rounded-md mb-2"
@click="onClickClose(true)"
>
That's enough help, thanks.
</button>
<button
type="button"
class="block w-full 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-2 py-3 rounded-md mb-2"
@click="$router.push({ name: 'discover' })"
>
Show me more!
</button>
</div>
</div>
<p class="mt-4 flex items-center">
To see these instructions and more, click above on
<span
class="ml-1 mr-1 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"
>
Help
</span>
</p>
</div>
<div v-if="page === OnboardPage.Discover" class="dialog">
<h1 class="text-xl font-bold text-center mb-4 relative">
Offer to Interesting Events & People
<div
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</h1>
<p>
Once you've seen things that others have given or done, you may find
ways you want to contribute, too. It turns out others have proposed
activities together, and this page is where you find projects.
</p>
<p class="mt-4">
Search for a topic, or search around your neighborhod under "Nearby".
</p>
<p class="mt-4">
When you find some that seem interesting, you can offer your help. You
are welcome to make your offer conditional, for example if they get 2
other people, too.
</p>
<p class="mt-4 flex items-center">
The
<fa
icon="magnifying-glass"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
button below brings you to this discovery screen.
</p>
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
type="button"
data-testId="closeOnboardingAndFinish"
class="block w-full text-center text-md 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-2 py-3 rounded-md mb-2"
@click="onClickClose(true)"
>
No more help, thanks.
</button>
<button
type="button"
class="block w-full 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-2 py-3 rounded-md mb-2"
@click="$router.push({ name: 'projects' })"
>
Show me even more.
</button>
</div>
</div>
</div>
<div v-if="page === OnboardPage.Create" class="dialog">
<h1 class="text-xl font-bold text-center mb-4 relative">
Fish for Others with Your Projects
<div
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</h1>
<p class="relative">
Now you can take a turn: click on the
<span class="bg-green-600 text-white rounded-full">
<fa icon="plus" class="fa-fw"></fa>
</span>
button to throw out projects of your own... anything you'd like to see
happen. If your first idea doesn't catch anyone, try, try again... and
let others know that this is a good place to find help.
</p>
<p class="mt-4 flex items-center">
The
<fa
icon="hand"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
button below brings you here to see your ideas.
</p>
<p class="mt-4">
By the way, one good way to get to know your neighbors and their
interests is to offer time directly to them. You can do this on the
contacts screen
<fa icon="users" class="text-slate-500" />
which is a great way to get to know a neighbor's interests.
</p>
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
type="button"
data-testId="closeOnboardingAndFinish"
class="block w-full text-center text-md 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-2 py-3 rounded-md mb-2"
@click="onClickClose(true, true)"
>
Let's go!
<br />
See & record gratitude.
</button>
<button
type="button"
class="block w-full 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-2 py-3 rounded-md mb-2"
@click="$router.push({ name: 'help' })"
>
I want to read more Help.
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { NotificationIface } from "@/constants/app";
import {
db,
retrieveSettingsForActiveAccount,
updateAccountSettings,
} from "@/db/index";
import { OnboardPage } from "@/libs/util";
@Component({
computed: {
OnboardPage() {
return OnboardPage;
},
},
components: { OnboardPage },
})
export default class OnboardingDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
firstContactName = null;
givenName = "";
isRegistered = false;
numContacts = 0;
page = OnboardPage.Home;
visible = false;
async open(page: OnboardPage) {
this.page = page;
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered;
const contacts = await db.contacts.toArray();
this.numContacts = contacts.length;
if (this.numContacts > 0) {
this.firstContactName = contacts[0].name;
}
this.visible = true;
if (this.page === OnboardPage.Create) {
// we'll assume that they've been through all the other pages
await updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
}
}
async onClickClose(done?: boolean, goHome?: boolean) {
this.visible = false;
if (done) {
await updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
if (goHome) {
(this.$router as Router).push({ name: "home" });
}
}
}
}
</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

@@ -76,8 +76,7 @@
</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:
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
@@ -127,7 +126,8 @@ import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { retrieveSettingsForActiveAccount } from "@/db/index";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
@Component({ components: { Camera, VuePictureCropper } })
@@ -151,8 +151,9 @@ export default class PhotoDialog extends Vue {
async mounted() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
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);

View File

@@ -1,568 +0,0 @@
<template>
<transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
leave-active-class="transition ease-in duration-500"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="isVisible"
class="fixed z-[100] top-0 inset-x-0 w-full 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 v-if="serviceWorkerReady && vapidKey" class="text-lg mb-4">
<span v-if="pushType === DAILY_CHECK_TITLE">
Would you like to be notified of new activity, up to once a day?
</span>
<span v-else>
Would you like to get a reminder message once a day?
</span>
</p>
<p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 5 seconds...
<fa icon="spinner" spin />
</p>
<div v-if="serviceWorkerReady && vapidKey">
<div v-if="pushType === DAILY_CHECK_TITLE">
<span>Yes, send me a message when there is new data for me</span>
</div>
<div v-else>
<span>Yes, send me this message:</span>
<!-- eslint-disable -->
<textarea
type="text"
id="push-message"
v-model="messageInput"
class="rounded border border-slate-400 mt-2 px-2 py-2 w-full"
maxlength="100"
></textarea
>
<!-- eslint-enable -->
<span class="w-full flex justify-between text-xs text-slate-500">
<span></span>
<span>(100 characters max)</span>
</span>
</div>
<div>
<span class="flex flex-row justify-center">
<span class="mt-2">... at: </span>
<input
type="number"
@change="checkHourInput"
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"
/>
<input
type="number"
@change="checkMinuteInput"
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
v-model="minuteInput"
/>
<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>
</div>
<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="
close();
turnOnNotifications();
"
>
Turn on Daily Message
</button>
</div>
<button
@click="close()"
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>
</transition>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "@/db/index";
import { urlBase64ToUint8Array } from "@/libs/crypto/vc/util";
import * as libsUtil from "@/libs/util";
// Example interface for error
interface ErrorResponse {
message: string;
}
// PushSubscriptionJSON is defined in the Push API https://www.w3.org/TR/push-api/#dom-pushsubscriptionjson
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
message?: string;
notifyTime: { utcHour: number; minute: number };
notifyType: string;
}
interface ServiceWorkerMessage {
type: string;
data: string;
}
interface ServiceWorkerResponse {
// Define the properties and their types
success: boolean;
message?: string;
}
interface VapidResponse {
data: {
vapidKey: string;
};
}
@Component
export default class PushNotificationPermission extends Vue {
// eslint-disable-next-line
$notify!: (notification: NotificationIface, timeout?: number) => Promise<() => void>;
DAILY_CHECK_TITLE = libsUtil.DAILY_CHECK_TITLE;
DIRECT_PUSH_TITLE = libsUtil.DIRECT_PUSH_TITLE;
callback: (success: boolean, time: string, message?: string) => void =
() => {};
hourAm = true;
hourInput = "8";
isVisible = false;
messageInput = "";
minuteInput = "00";
pushType = "";
serviceWorkerReady = false;
vapidKey = "";
async open(
pushType: string,
callback?: (success: boolean, time: string, message?: string) => void,
) {
this.callback = callback || this.callback;
this.isVisible = true;
this.pushType = pushType;
try {
const settings = await retrieveSettingsForActiveAccount();
let pushUrl = DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
}
if (pushUrl.startsWith("http://localhost")) {
logConsoleAndDb("Not checking for VAPID in this local environment.");
} else {
let responseData = "";
await this.axios
.get(pushUrl + "/web-push/vapid")
.then((response: VapidResponse) => {
this.vapidKey = response.data?.vapidKey || "";
logConsoleAndDb("Got vapid key: " + this.vapidKey);
responseData = JSON.stringify(response.data);
navigator.serviceWorker?.addEventListener(
"controllerchange",
() => {
logConsoleAndDb(
"New service worker is now controlling the page",
);
},
);
});
if (!this.vapidKey) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Could not set notifications.",
},
5000,
);
logConsoleAndDb(
"Error Setting Notifications: web push server response didn't have vapidKey: " +
responseData,
true,
);
}
}
} catch (error) {
if (window.location.host.startsWith("localhost")) {
logConsoleAndDb(
"Ignoring the error getting VAPID for local development.",
);
} else {
logConsoleAndDb(
"Got an error initializing notifications: " + JSON.stringify(error),
true,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notifications",
text: "Got an error setting notifications.",
},
5000,
);
}
}
// there may be a long pause here on first initialization
navigator.serviceWorker?.ready.then(() => {
this.serviceWorkerReady = true;
});
if (this.pushType === this.DIRECT_PUSH_TITLE) {
this.messageInput =
"Just a friendly reminder: click and share some gratitude with the world.";
// focus on the message input
setTimeout(function () {
document.getElementById("push-message")?.focus();
}, 100);
} else {
// not critical but doesn't make sense in a daily check
this.messageInput = "";
}
}
private close() {
this.isVisible = false;
}
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> {
logConsoleAndDb(
"Requesting permission for notifications: " + JSON.stringify(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) => {
logConsoleAndDb(
"Response from service worker: " + JSON.stringify(response),
);
});
}
private checkNotificationSupport(): Promise<void> {
if (!("Notification" in window)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Browser Notifications Are Not Supported",
text: "This browser does not support notifications.",
},
3000,
);
return Promise.reject("This browser does not support notifications.");
}
if (window.Notification.permission === "granted") {
return Promise.resolve();
}
return Promise.resolve();
}
private requestNotificationPermission(): Promise<NotificationPermission> {
return window.Notification.requestPermission().then(
(permission: string) => {
if (permission !== "granted") {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Requesting Notification Permission",
text:
"Allow this app permission to make notifications for personal reminders." +
" You can adjust them at any time in your settings.",
},
-1,
);
throw new Error("We weren't granted permission.");
}
return permission;
},
);
}
private checkHourInput() {
const hourNum = parseInt(this.hourInput);
if (isNaN(hourNum)) {
this.hourInput = "12";
} else if (hourNum < 1) {
this.hourInput = "12";
this.hourAm = !this.hourAm;
} else if (hourNum > 12) {
this.hourInput = "1";
this.hourAm = !this.hourAm;
} else {
this.hourInput = hourNum.toString();
}
}
private checkMinuteInput() {
const minuteNum = parseInt(this.minuteInput);
if (isNaN(minuteNum)) {
this.minuteInput = "00";
} else if (minuteNum < 0) {
this.minuteInput = "59";
} else if (minuteNum < 10) {
this.minuteInput = "0" + minuteNum;
} else if (minuteNum > 59) {
this.minuteInput = "00";
} else {
this.minuteInput = minuteNum.toString();
}
}
private async turnOnNotifications() {
let notifyCloser = () => {};
return this.askPermission()
.then((permission) => {
logConsoleAndDb("Permission granted: " + JSON.stringify(permission));
// Call the function and handle promises
return this.subscribeToPush();
})
.then(() => {
logConsoleAndDb("Subscribed successfully.");
return navigator.serviceWorker?.ready;
})
.then((registration) => {
return registration.pushManager.getSubscription();
})
.then(async (subscription) => {
if (subscription) {
notifyCloser = 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 = this.hourAm
? // If it's AM, then we'll change it to 0 for 12 AM but otherwise use rawHourNum
rawHourNum === 12
? 0
: rawHourNum
: // Otherwise it's PM, so keep a 12 but otherwise add 12
rawHourNum === 12
? 12
: rawHourNum + 12;
const hourNum = adjHourNum % 24; // probably unnecessary now
const utcHour =
hourNum + Math.round(new Date().getTimezoneOffset() / 60);
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
const minuteNum = libsUtil.numberOrZero(this.minuteInput);
const utcMinute =
minuteNum + Math.round(new Date().getTimezoneOffset() % 60);
const finalUtcMinute = (utcMinute + (utcMinute < 0 ? 60 : 0)) % 60;
const subscriptionWithTime: PushSubscriptionWithTime = {
notifyTime: { utcHour: finalUtcHour, minute: finalUtcMinute },
notifyType: this.pushType,
message: this.messageInput,
...subscription.toJSON(),
};
await this.sendSubscriptionToServer(subscriptionWithTime);
// To help investigate potential issues with this: https://firebase.google.com/docs/cloud-messaging/migrate-v1
logConsoleAndDb(
"Subscription data sent to server with endpoint: " +
subscription.endpoint,
);
return subscriptionWithTime;
} else {
throw new Error("Subscription object is not available.");
}
})
.then(async (subscription: PushSubscriptionWithTime) => {
logConsoleAndDb(
"Subscription data sent to server and all finished successfully.",
);
await libsUtil.sendTestThroughPushServer(subscription, true);
notifyCloser();
setTimeout(() => {
this.$notify(
{
group: "alert",
type: "success",
title: "Notification Is On",
text: "You should see at least one on your device; if not, check the 'Troubleshoot' link.",
},
7000,
);
}, 500);
const timeText =
// eslint-disable-next-line
this.hourInput + ":" + this.minuteInput + " " + (this.hourAm ? "AM" : "PM");
this.callback(true, timeText, this.messageInput);
})
.catch((error) => {
logConsoleAndDb(
"Got an error setting notification permissions: " +
" string " +
error.toString() +
" JSON " +
JSON.stringify(error),
true,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Notification Permissions",
text: "Could not set notification permissions.",
},
3000,
);
// if we want to also unsubscribe, be sure to do that only if no other notification is active
});
}
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 (window.Notification.permission !== "granted") {
const errorMsg = "Notification permission not granted";
console.warn(errorMsg);
return reject(new Error(errorMsg));
}
const applicationServerKey = urlBase64ToUint8Array(this.vapidKey);
const options: PushSubscriptionOptions = {
userVisibleOnly: true,
applicationServerKey: applicationServerKey,
};
navigator.serviceWorker?.ready
.then((registration) => {
return registration.pushManager.subscribe(options);
})
.then((subscription) => {
logConsoleAndDb(
"Push subscription successful: " + JSON.stringify(subscription),
);
resolve();
})
.catch((error) => {
logConsoleAndDb(
"Push subscription failed: " +
JSON.stringify(error) +
" - " +
JSON.stringify(options),
true,
);
// Inform the user about the issue
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Push Notifications",
text:
"We encountered an issue setting up push notifications. " +
"If you wish to revoke notification permissions, please do so in your browser settings.",
},
-1,
);
reject(error);
});
});
}
private sendSubscriptionToServer(
subscription: PushSubscriptionWithTime,
): Promise<void> {
logConsoleAndDb(
"About to send subscription... " + JSON.stringify(subscription),
);
return fetch("/web-push/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(subscription),
}).then((response) => {
if (!response.ok) {
console.error("Bad response subscribing to web push: ", response);
throw new Error("Failed to send push subscription to server");
}
logConsoleAndDb("Push subscription sent to server successfully.");
});
}
}
</script>
<style scoped>
/* Add any specific styles for this component here */
</style>

View File

@@ -16,7 +16,8 @@
import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "@/constants/app";
import { retrieveSettingsForActiveAccount } from "@/db/index";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component
export default class TopMessage extends Vue {
@@ -28,15 +29,17 @@ export default class TopMessage extends Vue {
async mounted() {
try {
const settings = await retrieveSettingsForActiveAccount();
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
if (
settings.warnIfTestServer &&
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?.warnIfProdServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);

View File

@@ -1,11 +1,10 @@
<!-- similar to ContactNameDialog -->
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Set Your Name</h1>
This is not sent to servers. It is only shared with people when you send
it to them.
Note that this is not sent to servers. It is only shared with people when
you choose to send it to them.
<input
type="text"
placeholder="Name"
@@ -22,6 +21,7 @@
>
Save
</button>
<!-- SHOW ME instead while processing saving changes -->
<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-2 py-3 rounded-md mb-2"
@@ -39,24 +39,22 @@
import { Vue, Component } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component
export default class UserNameDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
callback: (name: string) => void = () => {};
callback: (string?) => void = () => {};
givenName = "";
visible = false;
/**
* @param aCallback - callback function for name, which may be ""
*/
async open(aCallback?: (name: string) => void) {
async open(aCallback?: (name?: string) => void) {
this.callback = aCallback || this.callback;
const settings = await retrieveSettingsForActiveAccount();
this.givenName = settings.firstName || "";
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.givenName = settings?.firstName || "";
this.visible = true;
}

View File

@@ -3,7 +3,8 @@ 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 { retrieveSettingsForActiveAccount } from "@/db";
import { db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { getHeaders } from "@/libs/endorserServer";
const ANIMATION_DURATION_SECS = 10;
@@ -13,9 +14,10 @@ export async function loadLandmarks(vue, world, scene, loop) {
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
try {
const settings = await retrieveSettingsForActiveAccount();
const activeDid = settings.activeDid || "";
const apiServer = settings.apiServer;
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer;
const headers = await getHeaders(activeDid);
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";

View File

@@ -12,24 +12,17 @@ export enum AppString {
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
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",
PROD_PARTNER_API_SERVER = "https://partner-api.endorser.ch",
TEST_PARTNER_API_SERVER = "https://test-partner-api.endorser.ch",
LOCAL_PARTNER_API_SERVER = LOCAL_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 APP_SERVER =
import.meta.env.VITE_APP_SERVER || "https://timesafari.app";
export const DEFAULT_ENDORSER_API_SERVER =
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
AppString.TEST_ENDORSER_API_SERVER;
@@ -38,10 +31,6 @@ export const DEFAULT_IMAGE_API_SERVER =
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
AppString.TEST_IMAGE_API_SERVER;
export const DEFAULT_PARTNER_API_SERVER =
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
AppString.TEST_PARTNER_API_SERVER;
export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host;
@@ -52,14 +41,13 @@ export const PASSKEYS_ENABLED =
/**
* The possible values for "group" and "type" are in App.vue.
* Some of this comes from the notiwind package, some is custom.
* From the notiwind package
*/
export interface NotificationIface {
group: string; // "alert" | "modal"
type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string;
text?: string;
callback?: (success: boolean) => Promise<void>; // if this triggered an action
noText?: string;
onCancel?: (stopAsking?: boolean) => Promise<void>;
onNo?: (stopAsking?: boolean) => Promise<void>;

View File

@@ -1,7 +1,5 @@
import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import * as R from "ramda";
import { Account, AccountsSchema } from "./tables/accounts";
import { Contact, ContactSchema } from "./tables/contacts";
import { Log, LogSchema } from "./tables/logs";
@@ -47,111 +45,15 @@ accountsDB.version(1).stores(AccountsSchema);
db.version(2).stores({
...ContactSchema,
...LogSchema,
...{ settings: "id" }, // old Settings schema
...SettingsSchema,
});
// v3 added Temp
db.version(3).stores(TempSchema);
db.version(4)
.stores(SettingsSchema)
.upgrade((tx) => {
return tx
.table("settings")
.toCollection()
.modify((settings) => {
settings.accountDid = ""; // make it non-null for the default master settings, but still indexable
});
});
const DEFAULT_SETTINGS = {
id: MASTER_SETTINGS_KEY,
activeDid: undefined,
apiServer: DEFAULT_ENDORSER_API_SERVER,
};
// Event handler to initialize the non-sensitive database with default settings
db.on("populate", async () => {
await db.settings.add(DEFAULT_SETTINGS);
await db.settings.add({
id: MASTER_SETTINGS_KEY,
apiServer: DEFAULT_ENDORSER_API_SERVER,
});
});
// retrieves default settings
// calls db.open()
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
await db.open();
return (await db.settings.get(MASTER_SETTINGS_KEY)) || DEFAULT_SETTINGS;
}
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
const defaultSettings = await retrieveSettingsForDefaultAccount();
if (!defaultSettings.activeDid) {
return defaultSettings;
} else {
const overrideSettings =
(await db.settings
.where("accountDid")
.equals(defaultSettings.activeDid)
.first()) || {};
return R.mergeDeepRight(defaultSettings, overrideSettings);
}
}
// Update settings for the given account, or in MASTER_SETTINGS_KEY if no accountDid is provided.
// Don't expose this because we should be explicit on whether we're updating the default settings or account settings.
async function updateSettings(settingsChanges: Settings): Promise<void> {
await db.open();
if (!settingsChanges.accountDid) {
// ensure there is no "id" that would override the key
delete settingsChanges.id;
await db.settings.update(MASTER_SETTINGS_KEY, settingsChanges);
} else {
const result = await db.settings
.where("accountDid")
.equals(settingsChanges.accountDid)
.modify(settingsChanges);
if (result === 0) {
if (!settingsChanges.id) {
// It is unfortunate that we have to set this explicitly.
// We didn't make id a "++id" at the beginning and Dexie won't let us change it,
// plus we made our first settings objects MASTER_SETTINGS_KEY = 1 instead of 0
settingsChanges.id = (await db.settings.count()) + 1;
}
await db.settings.add(settingsChanges);
}
}
}
export async function updateDefaultSettings(settings: Settings): Promise<void> {
delete settings.accountDid; // just in case
await updateSettings(settings);
}
export async function updateAccountSettings(
accountDid: string,
settings: Settings,
): Promise<void> {
settings.accountDid = accountDid;
await updateSettings(settings);
}
// similar method is in the sw_scripts/additional-scripts.js file
export async function logConsoleAndDb(
message: string,
isError = false,
): Promise<void> {
if (isError) {
console.error(`${new Date().toISOString()} ${message}`);
} else {
console.log(`${new Date().toISOString()} ${message}`);
}
await db.open();
const todayKey = new Date().toDateString();
// only keep one day's worth of logs
const previous = await db.logs.get(todayKey);
if (!previous) {
// when this is today's first log, clear out everything previous
await db.logs.clear();
}
const prevMessages = (previous && previous.message) || "";
const fullMessage = `${prevMessages}\n${new Date().toISOString()} ${message}`;
await db.logs.update(todayKey, { message: fullMessage });
}

View File

@@ -1 +0,0 @@
Check the contact & settings export to see whether you want your new table to be included in it.

View File

@@ -12,42 +12,24 @@ export type BoundingBox = {
* Settings type encompasses user-specific configuration details.
*/
export type Settings = {
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID
id?: number; // this is only blank on input, when the database assigns it
// if supplied, this settings record overrides the master record when the user switches to this account
accountDid?: string; // not used in the MASTER_SETTINGS_KEY entry
// active Decentralized ID
activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry
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
finishedOnboarding?: boolean; // the user has completed the onboarding process
firstName?: string; // user's full name, may be null if unwanted for a particular account
firstName?: string; // user's full name
hideRegisterPromptOnNewContact?: boolean;
isRegistered?: boolean;
imageServer?: string;
lastName?: string; // deprecated - put all names in firstName
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
// The claim list has a most recent one used in notifications that's separate from the last viewed
lastNotifiedClaimId?: string;
lastViewedClaimId?: string;
notifyingNewActivityTime?: string; // set to their chosen time if they have turned on daily check for new activity via the push server
notifyingReminderMessage?: string; // set to their chosen message for a daily reminder
notifyingReminderTime?: string; // set to their chosen time for a daily reminder
partnerApiServer?: string; // partner server API URL
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
profileImageUrl?: string; // may be null if unwanted for a particular account
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<{
@@ -64,7 +46,7 @@ export type Settings = {
webPushServer?: string; // Web Push server URL
};
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
export function isAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible);
}
@@ -72,7 +54,7 @@ export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
* Schema for the Settings table in the database.
*/
export const SettingsSchema = {
settings: "id, &accountDid",
settings: "id",
};
/**

View File

@@ -1,46 +0,0 @@
/**
* This did:ethr resolver instructs the did-jwt machinery to use the
* EcdsaSecp256k1RecoveryMethod2020Uses verification method which adds the recovery bit to the
* signature to recover the DID's public key from a signature.
*
* This effectively hard codes the did:ethr DID resolver to use the address as the public key.
* @param did : string
* @returns {Promise<DIDResolutionResult>}
*
* Similar code resides in image-api
*/
export const didEthLocalResolver = async (did: string) => {
const didRegex = /^did:ethr:(0x[0-9a-fA-F]{40})$/;
const match = did.match(didRegex);
if (match) {
const address = match[1]; // Extract eth address: 0x...
const publicKeyHex = address; // Use the address directly as a public key placeholder
return {
didDocumentMetadata: {},
didResolutionMetadata: {
contentType: "application/did+ld+json",
},
didDocument: {
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/secp256k1recovery-2020/v2",
],
id: did,
verificationMethod: [
{
id: `${did}#controller`,
type: "EcdsaSec256k1RecoveryMethod2020",
controller: did,
blockchainAccountId: "eip155:1:" + publicKeyHex,
},
],
authentication: [`${did}#controller`],
assertionMethod: [`${did}#controller`],
},
};
}
throw new Error(`Unsupported DID format: ${did}`);
};

View File

@@ -6,22 +6,14 @@
*
*/
import { Buffer } from "buffer/";
import * as didJwt from "did-jwt";
import { JWTVerified } from "did-jwt";
import { JWTDecoded } from "did-jwt/lib/JWT";
import { Resolver } from "did-resolver";
import { IIdentifier } from "@veramo/core";
import * as u8a from "uint8arrays";
import { didEthLocalResolver } from "./did-eth-local-resolver";
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
import { urlBase64ToUint8Array } from "./util";
import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer";
export const ETHR_DID_PREFIX = "did:ethr:";
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
/**
* Meta info about a key
@@ -41,8 +33,6 @@ export interface KeyMeta {
passkeyCredIdHex?: string;
}
const resolver = new Resolver({ ethr: didEthLocalResolver });
/**
* 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
@@ -54,22 +44,16 @@ export function isFromPasskey(keyMeta?: KeyMeta): boolean {
export async function createEndorserJwtForKey(
account: KeyMeta,
payload: object,
expiresIn?: number,
) {
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);
const options = {
return didJwt.createJWT(payload, {
issuer: account.did,
signer: signer,
expiresIn: undefined as number | undefined,
};
if (expiresIn) {
options.expiresIn = expiresIn;
}
return didJwt.createJWT(payload, options);
});
} else if (account?.passkeyCredIdHex) {
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
} else {
@@ -123,78 +107,6 @@ function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}
// We should be calling 'verify' in more places, showing warnings if it fails.
export function decodeEndorserJwt(jwt: string): JWTDecoded {
return didJwt.decodeJWT(jwt);
}
// return Promise of at least { issuer, payload, verified boolean }
// ... and also if successfully verified by did-jwt (not JWANT): data, doc, signature, signer
export async function decodeAndVerifyJwt(
jwt: string,
): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> {
const pieces = jwt.split(".");
console.log("WTF decodeAndVerifyJwt", typeof jwt, jwt, pieces);
const header = JSON.parse(base64urlDecodeString(pieces[0]));
const payload = JSON.parse(base64urlDecodeString(pieces[1]));
console.log("WTF decodeAndVerifyJwt after", header, payload);
const issuerDid = payload.iss;
if (!issuerDid) {
return Promise.reject({
clientError: {
message: `Missing "iss" field in JWT.`,
},
});
}
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
try {
const verified = await didJwt.verifyJWT(jwt, { resolver });
return verified;
} catch (e: unknown) {
return Promise.reject({
clientError: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
message: `JWT failed verification: ` + e.toString(),
code: JWT_VERIFY_FAILED_CODE,
},
});
}
}
if (issuerDid.startsWith(PEER_DID_PREFIX) && header.typ === "JWANT") {
const verified = await verifyPeerSignature(
Buffer.from(payload),
issuerDid,
urlBase64ToUint8Array(pieces[2]),
);
if (!verified) {
return Promise.reject({
clientError: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
message: `JWT failed verification: ` + e.toString(),
code: JWT_VERIFY_FAILED_CODE,
},
});
} else {
return { issuer: issuerDid, payload: payload, verified: true };
}
}
if (issuerDid.startsWith(PEER_DID_PREFIX)) {
return Promise.reject({
clientError: {
message: `JWT with a PEER DID currently only supported with typ == JWANT. Contact us us for JWT suport since it should be straightforward.`,
},
});
}
return Promise.reject({
clientError: {
message: `Unsupported DID method ${issuerDid}`,
code: UNSUPPORTED_DID_METHOD_CODE,
},
});
}

View File

@@ -470,18 +470,8 @@ ${pubKeyBuffer.toString("base64")}
return pem;
}
// tried the base64url library but got an error using their Buffer
export function base64urlDecodeString(input: string) {
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
}
// tried the base64url library but got an error using their Buffer
export function base64urlEncodeString(input: string) {
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlDecodeArrayBuffer(input: string) {
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);
@@ -493,9 +483,9 @@ function base64urlDecodeArrayBuffer(input: string) {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
function base64urlEncode(buffer: ArrayBuffer) {
const str = String.fromCharCode(...new Uint8Array(buffer));
return base64urlEncodeString(str);
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// from @simplewebauthn/browser

View File

@@ -1,11 +0,0 @@
export function 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;
}

View File

@@ -8,11 +8,7 @@ import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
import { Contact } from "@/db/tables/contacts";
import { accessToken, deriveAddress, nextDerivationPath } from "@/libs/crypto";
import { NonsensitiveDexie } from "@/db/index";
import {
getAccount,
getPasskeyExpirationSeconds,
GiverReceiverInputInfo,
} from "@/libs/util";
import { getAccount, getPasskeyExpirationSeconds } from "@/libs/util";
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
import { Account } from "@/db/tables/accounts";
@@ -36,6 +32,11 @@ export interface AgreeVerifiableCredential {
object: Record<string, any>;
}
export interface GiverReceiverInputInfo {
did?: string;
name?: string;
}
export interface GiverOutputInfo {
action: string;
giver?: GiverReceiverInputInfo;
@@ -49,7 +50,6 @@ export interface ClaimResult {
error: { code: string; message: string };
}
// similar to VerifiableCredentialSubject... maybe rename this
export interface GenericVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": string;
@@ -57,6 +57,8 @@ export interface GenericVerifiableCredential {
}
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
"@context": string;
"@type": string;
claim: T;
claimType?: string;
handleId: string;
@@ -67,6 +69,8 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
}
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
{
"@context": SCHEMA_ORG_CONTEXT,
"@type": "",
claim: { "@type": "" },
handleId: "",
id: "",
@@ -81,14 +85,11 @@ export interface GiveSummaryRecord {
amountConfirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
fulfillsHandleId: string;
fulfillsPlanHandleId?: string;
fulfillsType?: string;
fulfillsPlanHandleId: string;
handleId: string;
issuedAt: string;
issuerDid: string;
jwtId: string;
providerPlanHandleId?: string;
recipientDid: string;
unit: string;
}
@@ -112,10 +113,6 @@ export interface OfferSummaryRecord {
validThrough: string;
}
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
planName: string;
}
// a summary record; the VC is not currently part of this record
export interface PlanSummaryRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well
@@ -143,7 +140,6 @@ export interface GiveVerifiableCredential extends GenericVerifiableCredential {
identifier?: string;
image?: string;
object?: { amountOfThisGood: number; unitCode: string };
provider?: GenericVerifiableCredential; // typically @type & identifier
recipient?: { identifier: string };
}
@@ -224,21 +220,11 @@ export interface ImageRateLimits {
}
export interface VerifiableCredential {
exp?: number;
iat: number;
iss: string;
vc: {
"@context": string[];
type: string[];
credentialSubject: VerifiableCredentialSubject;
};
}
// similar to GenericVerifiableCredential... maybe replace that one
export interface VerifiableCredentialSubject {
"@context": string;
"@type": string;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
name: string;
description: string;
identifier?: string;
}
export interface WorldProperties {
@@ -246,14 +232,12 @@ export interface WorldProperties {
endTime?: string;
}
// AKA Registration & RegisterAction
export interface RegisterVerifiableCredential {
"@context": typeof SCHEMA_ORG_CONTEXT;
"@type": "RegisterAction";
"@context": string;
"@type": string;
agent: { identifier: string };
identifier?: string; // used for invites (when participant ID isn't known)
object: string;
participant?: { identifier: string }; // used when person is known (not an invite)
participant: { identifier: string };
}
// now for some of the error & other wrapper types
@@ -285,14 +269,6 @@ export interface ErrorResult extends ResultWithType {
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
export interface UserInfo {
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
// This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN";
@@ -544,7 +520,7 @@ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
* @param apiServer
*/
export async function getPlanFromCache(
handleId: string | undefined,
handleId: string | null,
axios: Axios,
apiServer: string,
requesterDid?: string,
@@ -591,52 +567,6 @@ export async function setPlanInCache(
planCache.set(handleId, planSummary);
}
/**
*
* @returns { data: Array<OfferSummaryRecord>, hitLimit: boolean true if maximum was hit and there may be more }
*/
export async function getNewOffersToUser(
axios: Axios,
apiServer: string,
activeDid: string,
afterOfferJwtId?: string,
beforeOfferJwtId?: string,
) {
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`;
if (afterOfferJwtId) {
url += "&afterId=" + afterOfferJwtId;
}
if (beforeOfferJwtId) {
url += "&beforeId=" + beforeOfferJwtId;
}
const headers = await getHeaders(activeDid);
const response = await axios.get(url, { headers });
return response.data;
}
/**
*
* @returns { data: Array<OfferToPlanSummaryRecord>, hitLimit: boolean true if maximum was hit and there may be more }
*/
export async function getNewOffersToUserProjects(
axios: Axios,
apiServer: string,
activeDid: string,
afterOfferJwtId?: string,
beforeOfferJwtId?: string,
) {
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`;
if (afterOfferJwtId) {
url += "?afterId=" + afterOfferJwtId;
}
if (beforeOfferJwtId) {
url += afterOfferJwtId ? "&" : "?";
url += "beforeId=" + beforeOfferJwtId;
}
const headers = await getHeaders(activeDid);
const response = await axios.get(url, { headers });
return response.data;
}
/**
* Construct GiveAction VC for submission to server
*
@@ -653,7 +583,6 @@ export function hydrateGive(
fulfillsOfferHandleId?: string,
isTrade: boolean = false,
imageUrl?: string,
providerPlanHandleId?: string,
lastClaimId?: string,
): GiveVerifiableCredential {
// Remember: replace values or erase if it's null
@@ -712,10 +641,6 @@ export function hydrateGive(
vcClaim.image = imageUrl || undefined;
vcClaim.provider = providerPlanHandleId
? { "@type": "PlanAction", identifier: providerPlanHandleId }
: undefined;
return vcClaim;
}
@@ -740,7 +665,6 @@ export async function createAndSubmitGive(
fulfillsOfferHandleId?: string,
isTrade: boolean = false,
imageUrl?: string,
providerPlanHandleId?: string,
): Promise<CreateAndSubmitClaimResult> {
const vcClaim = hydrateGive(
undefined,
@@ -753,7 +677,6 @@ export async function createAndSubmitGive(
fulfillsOfferHandleId,
isTrade,
imageUrl,
providerPlanHandleId,
undefined,
);
return createAndSubmitClaim(
@@ -786,7 +709,6 @@ export async function editAndSubmitGive(
fulfillsOfferHandleId?: string,
isTrade: boolean = false,
imageUrl?: string,
providerPlanHandleId?: string,
): Promise<CreateAndSubmitClaimResult> {
const vcClaim = hydrateGive(
fullClaim.claim,
@@ -799,7 +721,6 @@ export async function editAndSubmitGive(
fulfillsOfferHandleId,
isTrade,
imageUrl,
providerPlanHandleId,
fullClaim.id,
);
return createAndSubmitClaim(
@@ -1012,12 +933,17 @@ export async function generateEndorserJwtForAccount(
isRegistered?: boolean,
name?: string,
profileImageUrl?: string,
// note that including the next key pushes QR codes to the next resolution smaller
includeNextKeyIfDerived?: boolean,
) {
const publicKeyHex = account.publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
interface UserInfo {
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
const contactInfo = {
iat: Date.now(),
iss: account.did,
@@ -1031,7 +957,7 @@ export async function generateEndorserJwtForAccount(
contactInfo.own.profileImageUrl = profileImageUrl;
}
if (includeNextKeyIfDerived && account?.mnemonic && account?.derivationPath) {
if (account?.mnemonic && account?.derivationPath) {
const newDerivPath = nextDerivationPath(account.derivationPath as string);
const nextPublicHex = deriveAddress(
account.mnemonic as string,
@@ -1052,10 +978,9 @@ export async function generateEndorserJwtForAccount(
export async function createEndorserJwtForDid(
issuerDid: string,
payload: object,
expiresIn?: number,
) {
const account = await getAccount(issuerDid);
return createEndorserJwtForKey(account as KeyMeta, payload, expiresIn);
return createEndorserJwtForKey(account as KeyMeta, payload);
}
/**
@@ -1251,7 +1176,7 @@ export const claimSpecialDescription = (
export const BVC_MEETUPS_PROJECT_CLAIM_ID =
import.meta.env.VITE_BVC_MEETUPS_PROJECT_CLAIM_ID ||
"https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H"; // production value, which seems like the safest value if forgotten
"https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK"; // this won't resolve as a URL on production; it's a URN only found in the test system
export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
return {
@@ -1285,24 +1210,19 @@ export async function createEndorserJwtVcFromClaim(
return createEndorserJwtForDid(issuerDid, vcPayload);
}
export async function createInviteJwt(
export async function register(
activeDid: string,
contact?: Contact,
inviteId?: string,
expiresIn?: number,
): Promise<string> {
apiServer: string,
axios: Axios,
contact: Contact,
) {
const vcClaim: RegisterVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "RegisterAction",
agent: { identifier: activeDid },
object: SERVICE_ID,
participant: { identifier: contact.did },
};
if (contact) {
vcClaim.participant = { identifier: contact.did };
}
if (inviteId) {
vcClaim.identifier = inviteId;
}
// Make a payload for the claim
const vcPayload = {
vc: {
@@ -1312,17 +1232,7 @@ export async function createInviteJwt(
},
};
// Create a signature using private key of identity
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
return vcJwt;
}
export async function register(
activeDid: string,
apiServer: string,
axios: Axios,
contact: Contact,
): Promise<{ success?: boolean; error?: string }> {
const vcJwt = await createInviteJwt(activeDid, contact);
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload);
const url = apiServer + "/api/v2/claim";
const resp = await axios.post(url, { jwtEncoded: vcJwt });

View File

@@ -6,40 +6,25 @@ import * as R from "ramda";
import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import {
accountsDB,
retrieveSettingsForActiveAccount,
updateAccountSettings,
updateDefaultSettings,
} from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "@/db/tables/settings";
import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY,
} from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer";
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 interface GiverReceiverInputInfo {
did?: string;
name?: string;
}
export enum OnboardPage {
Home = "HOME",
Discover = "DISCOVER",
Create = "CREATE",
Contact = "CONTACT",
Account = "ACCOUNT",
}
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";
@@ -321,9 +306,9 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
publicKeyHex: newId.keys[0].publicKeyHex,
});
await updateDefaultSettings({ activeDid: newId.did });
//console.log("Updated default settings in util");
await updateAccountSettings(newId.did, { isRegistered: false });
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
return newId.did;
};
@@ -351,40 +336,45 @@ export const registerSaveAndActivatePasskey = async (
keyName: string,
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
await updateDefaultSettings({ activeDid: account.did });
await updateAccountSettings(account.did, { isRegistered: false });
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
return account;
};
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
const settings = await retrieveSettingsForActiveAccount();
return (
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const passkeyExpirationSeconds =
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
60
);
60;
return passkeyExpirationSeconds;
};
// These are shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
export const DAILY_CHECK_TITLE = "DAILY_CHECK";
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
export const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
export const sendTestThroughPushServer = async (
subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean,
): Promise<AxiosResponse> => {
const settings = await retrieveSettingsForActiveAccount();
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 = {
...subscriptionJSON,
// ... overridden with the following
// 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);

View File

@@ -39,12 +39,9 @@ import {
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
@@ -57,7 +54,6 @@ import {
faHouseChimney,
faImagePortrait,
faLeftRight,
faLightbulb,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
@@ -113,12 +109,9 @@ library.add(
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
@@ -131,7 +124,6 @@ library.add(
faHouseChimney,
faImagePortrait,
faLeftRight,
faLightbulb,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,

View File

@@ -103,11 +103,6 @@ const routes: Array<RouteRecordRaw> = [
name: "help-notifications",
component: () => import("../views/HelpNotificationsView.vue"),
},
{
path: "/help-notification-types",
name: "help-notification-types",
component: () => import("../views/HelpNotificationTypesView.vue"),
},
{
path: "/help-onboarding",
name: "help-onboarding",
@@ -133,16 +128,6 @@ const routes: Array<RouteRecordRaw> = [
name: "import-derive",
component: () => import("../views/ImportDerivedAccountView.vue"),
},
{
path: "/invite-one",
name: "invite-one",
component: () => import("../views/InviteOneView.vue"),
},
{
path: "/new-activity",
name: "new-activity",
component: () => import("../views/NewActivityView.vue"),
},
{
path: "/new-edit-account",
name: "new-edit-account",
@@ -189,16 +174,6 @@ const routes: Array<RouteRecordRaw> = [
name: "quick-action-bvc-end",
component: () => import("../views/QuickActionBvcEndView.vue"),
},
{
path: "/recent-offers-to-user",
name: "recent-offers-to-user",
component: () => import("../views/RecentOffersToUserView.vue"),
},
{
path: "/recent-offers-to-user-projects",
name: "recent-offers-to-user-projects",
component: () => import("../views/RecentOffersToUserProjectsView.vue"),
},
{
path: "/scan-contact",
name: "scan-contact",

20
src/store/app.ts Normal file
View File

@@ -0,0 +1,20 @@
// @ts-check
import { defineStore } from "pinia";
export const useAppStore = defineStore({
id: "app",
state: () => ({
_projectId:
typeof localStorage.getItem("projectId") === "undefined"
? ""
: localStorage.getItem("projectId"),
}),
getters: {
projectId: (state): string => state._projectId as string,
},
actions: {
async setProjectId(newProjectId: string) {
localStorage.setItem("projectId", newProjectId);
},
},
});

View File

@@ -1,9 +1,10 @@
import axios from "axios";
import * as didJwt from "did-jwt";
import { AppString } from "@/constants/app";
import { retrieveSettingsForActiveAccount } from "../db";
import { SERVICE_ID } from "@/libs/endorserServer";
import { deriveAddress, newIdentifier } from "@/libs/crypto";
import { db } from "../db";
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.
@@ -16,7 +17,8 @@ export async function testServerRegisterUser() {
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
const settings = await retrieveSettingsForActiveAccount();
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Make a claim
const vcClaim = {
@@ -24,7 +26,7 @@ export async function testServerRegisterUser() {
"@type": "RegisterAction",
agent: { did: identity0.did },
object: SERVICE_ID,
participant: { did: settings.activeDid },
participant: { did: settings?.activeDid },
};
// Make a payload for the claim
const vcPayload = {
@@ -51,7 +53,7 @@ export async function testServerRegisterUser() {
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer =
settings.apiServer || AppString.TEST_ENDORSER_API_SERVER;
settings?.apiServer || AppString.TEST_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/claim";
const headers = {
"Content-Type": "application/json",

View File

@@ -9,11 +9,24 @@
Your Identity
</h1>
<div class="flex justify-between mb-2 mt-4">
<span />
<span class="whitespace-nowrap">
<router-link
:to="{ name: 'contact-qr' }"
class="text-xs bg-slate-500 text-white px-1.5 py-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw"></fa>
</router-link>
</span>
<span />
</div>
<!-- ID notice -->
<div
v-if="!activeDid"
id="noticeBeforeShare"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Note:</b> Before you can share with others or take any action, you
@@ -30,18 +43,10 @@
<!-- Identity Details -->
<div
id="sectionIdentityDetails"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"
>
<div v-if="givenName">
<h2 class="text-xl font-semibold mb-2">
<span class="whitespace-nowrap">
<router-link
:to="{ name: 'contact-qr' }"
class="bg-slate-500 text-white px-1.5 py-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw text-xl"></fa>
</router-link>
</span>
{{ givenName }}
<router-link :to="{ name: 'new-edit-account' }">
<fa icon="pen" class="text-xs text-blue-500 ml-2 mb-1"></fa>
@@ -54,10 +59,7 @@
>
<button
@click="
() =>
(this.$refs.userNameDialog as UserNameDialog).open(
(name) => (this.givenName = name),
)
() => $refs.userNameDialog.open((name) => (this.givenName = name))
"
class="inline-block 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-4 py-2 rounded-md"
>
@@ -154,14 +156,11 @@
</div>
<!-- Registration notice -->
<!--
We won't show any loading indicator because it usually doesn't change anything.
We'll just pop the message in only if we discover that they need it.
-->
<!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
<div
v-if="!loadingLimits && !endorserLimits?.nextWeekBeginDateTime"
id="noticeBeforeAnnounce"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Note:</b> Before you can publicly announce a new project or time
@@ -181,54 +180,20 @@
>
<!-- label -->
<div class="mb-2 font-bold">Notifications</div>
<div class="flex items-center justify-between">
<div
v-if="!notificationMaybeChanged"
class="flex items-center justify-between cursor-pointer"
@click="showNotificationChoice()"
>
<!-- label -->
<div>
Reminder Notification
<fa
icon="question-circle"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click.stop="showReminderNotificationInfo"
/>
</div>
<div>App Notifications</div>
<!-- toggle -->
<div
class="relative ml-2 cursor-pointer"
@click="showReminderNotificationChoice()"
>
<!-- input -->
<input type="checkbox" v-model="notifyingReminder" 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>
<div v-if="notifyingReminder" class="w-full flex justify-between">
<span class="ml-8 mr-8">Message: "{{ notifyingReminderMessage }}"</span>
<span>{{ notifyingReminderTime.replace(" ", "&nbsp;") }}</span>
</div>
<div class="mt-2 flex items-center justify-between">
<!-- label -->
<div>
New Activity Notification
<fa
icon="question-circle"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click.stop="showNewActivityNotificationInfo"
/>
</div>
<!-- toggle -->
<div
class="relative ml-2 cursor-pointer"
@click="showNewActivityNotificationChoice()"
>
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="notifyingNewActivity"
v-model="isSubscribed"
name="toggleNotificationsInput"
class="sr-only"
/>
<!-- line -->
@@ -239,14 +204,14 @@
></div>
</div>
</div>
<div v-if="notifyingNewActivityTime" class="w-full text-right">
{{ notifyingNewActivityTime.replace(" ", "&nbsp;") }}
<div v-else>
Notification status may have changed. Refresh this page to see the
latest setting.
</div>
<router-link class="pl-4 text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notification setup.
</router-link>
</div>
<PushNotificationPermission ref="pushNotificationPermission" />
<div
id="sectionSearchLocation"
@@ -291,7 +256,10 @@
<b>{{ endorserLimits.doneRegistrationsThisMonth }} registrations</b>
out of <b>{{ endorserLimits.maxRegistrationsPerMonth }}</b> for this
month.
<i>(You cannot register anyone else on your first day.)</i>
<i
>(You can register nobody on your first day, and after that only one
a day in your first month.)</i
>
Your registration counter resets at
<b class="whitespace-nowrap">
{{ readableDate(endorserLimits.nextMonthBeginDateTime) }}
@@ -344,7 +312,7 @@
>
If no download happened yet, click again here to download now.
</a>
<div class="mt-4">
<div>
<p>
After the download, you can save the file in your preferred storage
location.
@@ -460,37 +428,24 @@
<div class="ml-4 mt-2">
<input type="file" @change="uploadImportFile" class="ml-2" />
<transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
leave-active-class="transition ease-in duration-500"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="showContactImport()" class="mt-4">
<div class="flex justify-center">
<button
class="block 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 mb-6"
@click="confirmSubmitImportFile()"
>
Overwrite Settings & Contacts
<br />
(which doesn't include Identifier Data)
</button>
</div>
<div class="flex justify-center">
<button
class="block 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 mb-6"
@click="checkContactImports()"
>
Import Only Contacts
<br />
after comparing
</button>
</div>
</div>
</transition>
<div v-if="showContactImport()" class="mt-4">
<button
class="block 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 mb-6"
@click="confirmSubmitImportFile()"
>
Overwrite Settings & Contacts
<br />
(which doesn't include Identifier Data)
</button>
<button
class="block 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 mb-6"
@click="checkContactImports()"
>
Import Contacts
<br />
after comparing
</button>
</div>
</div>
</div>
@@ -637,45 +592,6 @@
{{ DEFAULT_PUSH_SERVER }}
</span>
<h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
<div class="px-3 py-4">
<input
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="partnerApiServerInput"
/>
<button
v-if="partnerApiServerInput != partnerApiServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSavePartnerServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="partnerApiServerInput = AppConstants.PROD_PARTNER_API_SERVER"
>
Use Prod
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="partnerApiServerInput = AppConstants.TEST_PARTNER_API_SERVER"
>
Use Test
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="partnerApiServerInput = AppConstants.LOCAL_PARTNER_API_SERVER"
>
Use Local
</button>
</div>
<span class="px-4 text-sm" v-if="!partnerApiServerInput">
When that setting is blank, this app will use the default partner server
URL:
{{ DEFAULT_PARTNER_API_SERVER }}
</span>
<div id="sectionImageServerURL" class="mt-2">
<span class="text-slate-500 text-sm font-bold">Image Server URL</span>
&nbsp;
@@ -805,29 +721,23 @@ import { useClipboard } from "@vueuse/core";
import EntityIcon from "@/components/EntityIcon.vue";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import PushNotificationPermission from "@/components/PushNotificationPermission.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import UserNameDialog from "@/components/UserNameDialog.vue";
import {
AppString,
DEFAULT_IMAGE_API_SERVER,
DEFAULT_PARTNER_API_SERVER,
DEFAULT_PUSH_SERVER,
IMAGE_TYPE_PROFILE,
NotificationIface,
} from "@/constants/app";
import {
db,
accountsDB,
retrieveSettingsForActiveAccount,
updateAccountSettings,
} from "@/db/index";
import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import {
clearPasskeyToken,
@@ -839,7 +749,7 @@ import {
ImageRateLimits,
tokenExpiryTimeDescription,
} from "@/libs/endorserServer";
import { DAILY_CHECK_TITLE, DIRECT_PUSH_TITLE, getAccount } from "@/libs/util";
import { getAccount } from "@/libs/util";
const inputImportFileNameRef = ref<Blob>();
@@ -847,7 +757,6 @@ const inputImportFileNameRef = ref<Blob>();
components: {
EntityIcon,
ImageMethodDialog,
PushNotificationPermission,
QuickNav,
TopMessage,
UserNameDialog,
@@ -859,7 +768,6 @@ export default class AccountViewView extends Vue {
AppConstants = AppString;
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;
DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_API_SERVER;
activeDid = "";
apiServer = "";
@@ -872,15 +780,10 @@ export default class AccountViewView extends Vue {
imageLimits: ImageRateLimits | null = null;
imageServer = "";
isRegistered = false;
isSubscribed = false;
limitsMessage = "";
loadingLimits = false;
notifyingNewActivity = false;
notifyingNewActivityTime = "";
notifyingReminder = false;
notifyingReminderMessage = "";
notifyingReminderTime = "";
partnerApiServer = "";
partnerApiServerInput = "";
notificationMaybeChanged = false;
passkeyExpirationDescription = "";
passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
@@ -921,15 +824,10 @@ export default class AccountViewView extends Vue {
/**
* Beware! I've seen where this "ready" never resolves.
*/
const registration = await navigator.serviceWorker?.ready;
const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription();
if (!this.subscription) {
if (this.notifyingNewActivity || this.notifyingReminder) {
// the app thought there was a subscription but there isn't, so fix the settings
this.turnOffNotifyingFlags();
}
}
// console.log("Got to the end of 'mounted' call in AccountViewView.");
this.isSubscribed = !!this.subscription;
console.log("Got to the end of 'mounted' call.");
/**
* Beware! I've seen where we never get to this point because "ready" never resolves.
*/
@@ -967,36 +865,31 @@ export default class AccountViewView extends Vue {
*/
async initializeState() {
await db.open();
const settings = await retrieveSettingsForActiveAccount();
const settings: Settings | undefined =
await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.apiServerInput = settings.apiServer || "";
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || "";
this.givenName =
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact;
this.isRegistered = !!settings?.isRegistered;
this.imageServer = settings.imageServer || "";
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime;
this.notifyingReminderMessage = settings.notifyingReminderMessage || "";
this.notifyingReminderTime = settings.notifyingReminderTime || "";
this.partnerApiServer = settings.partnerApiServer || "";
this.partnerApiServerInput = settings.partnerApiServer || "";
this.profileImageUrl = settings.profileImageUrl;
this.showContactGives = !!settings.showContactGivesInline;
this.imageServer = (settings?.imageServer as string) || "";
this.profileImageUrl = settings?.profileImageUrl as string;
this.showContactGives = !!settings?.showContactGivesInline;
this.hideRegisterPromptOnNewContact =
!!settings?.hideRegisterPromptOnNewContact;
this.passkeyExpirationMinutes =
settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
(settings?.passkeyExpirationMinutes as number) ??
DEFAULT_PASSKEY_EXPIRATION_MINUTES;
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.showShortcutBvc = !!settings.showShortcutBvc;
this.warnIfProdServer = !!settings.warnIfProdServer;
this.warnIfTestServer = !!settings.warnIfTestServer;
this.webPushServer = settings.webPushServer || "";
this.webPushServerInput = settings.webPushServer || "";
this.showGeneralAdvanced = !!settings?.showGeneralAdvanced;
this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || "";
this.webPushServerInput = (settings?.webPushServer as string) || "";
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds
@@ -1007,44 +900,29 @@ export default class AccountViewView extends Vue {
.then(() => setTimeout(fn, 2000));
}
async toggleShowContactAmounts() {
toggleShowContactAmounts() {
this.showContactGives = !this.showContactGives;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: this.showContactGives,
});
this.updateShowContactAmounts();
}
async toggleShowGeneralAdvanced() {
toggleShowGeneralAdvanced() {
this.showGeneralAdvanced = !this.showGeneralAdvanced;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showGeneralAdvanced: this.showGeneralAdvanced,
});
this.updateShowGeneralAdvanced();
}
async toggleProdWarning() {
toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: this.warnIfProdServer,
});
this.updateWarnIfProdServer(this.warnIfProdServer);
}
async toggleTestWarning() {
toggleTestWarning() {
this.warnIfTestServer = !this.warnIfTestServer;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: this.warnIfTestServer,
});
this.updateWarnIfTestServer(this.warnIfTestServer);
}
async toggleShowShortcutBvc() {
toggleShowShortcutBvc() {
this.showShortcutBvc = !this.showShortcutBvc;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: this.showShortcutBvc,
});
this.updateShowShortcutBvc(this.showShortcutBvc);
}
readableDate(timeStr: string) {
@@ -1069,127 +947,73 @@ export default class AccountViewView extends Vue {
}
}
async showNewActivityNotificationInfo() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "New Activity Notification",
text: `
This will only notify you when there is new relevant activity for you personally.
Note that it runs on your device and many factors may affect delivery,
so if you want a reliable but simple daily notification then choose a 'Reminder'.
Do you want more details?
`,
onYes: async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
async showNotificationChoice() {
if (!this.subscription) {
this.$notify(
{
group: "modal",
type: "notification-permission",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
yesText: "tell me more.",
},
-1,
);
-1,
);
} else {
this.$notify(
{
group: "modal",
type: "notification-off",
title: "", // unused, only here to satisfy type check
text: "", // unused, only here to satisfy type check
},
-1,
);
}
this.notificationMaybeChanged = true;
}
async showNewActivityNotificationChoice() {
if (!this.notifyingNewActivity) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingNewActivityTime: timeText,
});
this.notifyingNewActivity = true;
this.notifyingNewActivityTime = timeText;
}
public async updateShowContactAmounts() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: this.showContactGives,
});
}
public async updateShowGeneralAdvanced() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showGeneralAdvanced: this.showGeneralAdvanced,
});
}
public async updateWarnIfProdServer(newSetting: boolean) {
try {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting,
});
} else {
} catch (err) {
this.$notify(
{
group: "modal",
type: "notification-off",
title: DAILY_CHECK_TITLE, // repurposed to indicate the type of notification
text: "", // unused, only here to satisfy type check
callback: async (success) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingNewActivityTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
}
},
group: "alert",
type: "danger",
title: "Error Updating Prod Warning",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after prod-server-warning setting update because:",
err,
);
}
}
async showReminderNotificationInfo() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Reminder Notification",
text: `
This will notify you at a specific time each day.
Note that it does not give you personalized notifications,
so if you want less reliable but personalized notification then choose a 'New Activity' Notification.
Do you want more details?
`,
onYes: async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
yesText: "tell me more.",
},
-1,
);
}
async showReminderNotificationChoice() {
if (!this.notifyingReminder) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
).open(
DIRECT_PUSH_TITLE,
async (success: boolean, timeText: string, message?: string) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
this.notifyingReminder = true;
this.notifyingReminderMessage = message || "";
this.notifyingReminderTime = timeText;
}
},
);
} else {
this.$notify(
{
group: "modal",
type: "notification-off",
title: DIRECT_PUSH_TITLE, // repurposed to indicate the type of notification
text: "", // unused, only here to satisfy type check
callback: async (success) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
},
},
-1,
);
}
public async updateWarnIfTestServer(newSetting: boolean) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting,
});
}
public async toggleHideRegisterPromptOnNewContact() {
@@ -1210,19 +1034,11 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
}
public async turnOffNotifyingFlags() {
// should tell the push server as well
public async updateShowShortcutBvc(newSetting: boolean) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingNewActivityTime: "",
notifyingReminderMessage: "",
notifyingReminderTime: "",
showShortcutBvc: newSetting,
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
/**
@@ -1315,16 +1131,16 @@ export default class AccountViewView extends Vue {
* @param {Error} error - The error object.
*/
private handleExportError(error: unknown) {
console.error("Export Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Export Error",
text: "There was an error exporting the data.",
text: "See console logs for more info.",
},
-1,
);
console.error("Export Error:", error);
}
async uploadImportFile(event: Event) {
@@ -1405,7 +1221,7 @@ export default class AccountViewView extends Vue {
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
);
if (progress.done) {
// console.log(`Imported ${progress.completedTables} tables.`);
console.log(`Imported ${progress.completedTables} tables.`);
this.$notify(
{
group: "alert",
@@ -1448,7 +1264,10 @@ export default class AccountViewView extends Vue {
if (!this.isRegistered) {
// the user was not known to be registered, but now they are (because we got no error) so let's record it
try {
await updateAccountSettings(did, { isRegistered: true });
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: true,
});
this.isRegistered = true;
} catch (err) {
console.error("Got an error updating settings:", err);
@@ -1571,14 +1390,6 @@ export default class AccountViewView extends Vue {
this.apiServer = this.apiServerInput;
}
async onClickSavePartnerServer() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
partnerApiServer: this.partnerApiServerInput,
});
this.partnerApiServer = this.partnerApiServerInput;
}
async onClickSavePushServer() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
@@ -1667,7 +1478,8 @@ export default class AccountViewView extends Vue {
if ((error as any).response.status === 404) {
console.error("The image was already deleted:", error);
await updateAccountSettings(this.activeDid, {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: undefined,
});

View File

@@ -33,7 +33,8 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { NotificationIface } from "@/constants/app";
import { retrieveSettingsForActiveAccount } from "@/db/index";
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";
@@ -49,9 +50,10 @@ export default class ClaimAddRawView extends Vue {
claimStr = "";
async mounted() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
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 {
@@ -87,7 +89,7 @@ export default class ClaimAddRawView extends Vue {
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the claim.",
text: "There was a problem submitting the claim. See logs for more info.",
},
-1,
);

View File

@@ -36,6 +36,21 @@
</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" />
{{
@@ -45,7 +60,21 @@
</div>
<div>
<fa icon="user" class="fa-fw text-slate-400" />
{{ didInfo(veriClaim.issuer) }}
{{ 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" />
@@ -57,19 +86,10 @@
</a>
</div>
<div v-if="veriClaim.claimType === 'PlanAction'" class="mt-4">
<router-link
:to="'/project/' + encodeURIComponent(veriClaim.handleId)"
class="text-blue-500 mt-2"
>
Go to Project page
</router-link>
</div>
<!-- Fullfills Links -->
<!-- fullfills links for a give -->
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4">
<div v-if="detailsForGive?.fulfillsPlanHandleId">
<router-link
:to="
'/project/' +
@@ -93,7 +113,7 @@
@click="
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
"
class="text-blue-500 mt-4 cursor-pointer"
class="text-blue-500 mt-4"
>
Fulfills
{{
@@ -116,36 +136,6 @@
Offered to a bigger plan...
</router-link>
</div>
<!-- Providers -->
<div v-if="providersForGive?.length > 0" class="mt-4">
<span>Other assistance provided by:</span>
<ul class="ml-4">
<li
v-for="provider of providersForGive"
:key="provider.identifier"
class="list-disc ml-4"
>
<div class="flex gap-4">
<div class="grow overflow-hidden">
<a
@click="
provider.identifier.startsWith('did:')
? this.$router.push(
'/did/' +
encodeURIComponent(provider.identifier),
)
: showDifferentClaimPage(provider.identifier)
"
class="text-blue-500 mt-4 cursor-pointer"
>
an activity...
</a>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
@@ -161,7 +151,6 @@
<fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" />
</button>
</div>
<GiftedDialog ref="customGiveDialog" />
<div v-if="libsUtil.isGiveAction(veriClaim)">
<div class="flex columns-3">
@@ -193,6 +182,7 @@
</router-link>
</span>
</div>
<GiftedDialog ref="customGiveDialog" />
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
<span v-else-if="totalConfirmers() === 1">
@@ -303,7 +293,6 @@
</div>
</div>
<!-- Note that a similar section is found in ConfirmGiftView.vue -->
<div>
<h2 class="font-bold uppercase text-xl mt-8 mb-2">
{{ serverUtil.containsHiddenDid(veriClaim) ? "Visible " : "" }}Details
@@ -331,7 +320,7 @@
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('A link to this page', windowLocation)"
@click="copyToClipboard('This page location', windowLocation)"
class="text-blue-500"
>share this page with them</a
>
@@ -352,7 +341,7 @@
<span v-else>
If you'd like an introduction,
<a
@click="copyToClipboard('A link to this page', windowLocation)"
@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
@@ -465,22 +454,19 @@ import { useClipboard } from "@vueuse/core";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
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";
interface ProviderInfo {
identifier: string; // could be a DID or a handleId
linkConfirmed: boolean;
}
@Component({
components: { GiftedDialog, QuickNav },
})
@@ -504,7 +490,7 @@ export default class ClaimView extends Vue {
isEditedGlobalId = false;
isRegistered = false;
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
providersForGive: ProviderInfo[] = [];
showDidCopy = false;
showIdCopy = false;
showVeriClaimDump = false;
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
@@ -527,19 +513,19 @@ export default class ClaimView extends Vue {
this.fullClaimDump = "";
this.fullClaimMessage = "";
this.isEditedGlobalId = false;
this.isRegistered = false;
this.numConfsNotVisible = 0;
this.providersForGive = [];
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
this.veriClaimDump = "";
this.veriClaimDidsVisible = {};
}
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
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;
this.isRegistered = settings?.isRegistered || false;
await accountsDB.open();
const accounts = accountsDB.accounts;
@@ -637,39 +623,11 @@ export default class ClaimView extends Vue {
const giveResp = await this.axios.get(giveUrl, {
headers: giveHeaders,
});
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
if (giveResp.status === 200) {
this.detailsForGive = giveResp.data.data[0];
} else {
console.error("Error getting detailed give info:", giveResp);
}
// look for providers
const providerUrl =
this.apiServer +
"/api/v2/report/providersToGive?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string);
const providerHeaders = await serverUtil.getHeaders(userDid);
const providerResp = await this.axios.get(providerUrl, {
headers: providerHeaders,
});
// should be at least an empty array
if (
providerResp.status === 200 &&
Array.isArray(providerResp.data.data)
) {
this.providersForGive = providerResp.data.data;
} else {
console.error("Error getting give providers:", giveResp);
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: "Got error retrieving linked provider data.",
},
-1,
);
}
} else if (this.veriClaim.claimType === "Offer") {
const offerUrl =
this.apiServer +
@@ -683,15 +641,6 @@ export default class ClaimView extends Vue {
this.detailsForOffer = offerResp.data.data[0];
} else {
console.error("Error getting detailed offer info:", offerResp);
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: "Got error retrieving linked offer data.",
},
-1,
);
}
}
@@ -759,7 +708,7 @@ export default class ClaimView extends Vue {
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem getting that claim.",
text: "There was a problem getting that claim. See logs for more info.",
},
-1,
);
@@ -781,7 +730,7 @@ export default class ClaimView extends Vue {
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving that claim.",
text: "Something went wrong retrieving that claim. See logs for more info.",
},
-1,
);
@@ -844,7 +793,7 @@ export default class ClaimView extends Vue {
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation.",
text: "There was a problem submitting the confirmation. See logs for more info.",
},
-1,
);
@@ -862,12 +811,11 @@ export default class ClaimView extends Vue {
}
openFulfillGiftDialog() {
const giver: libsUtil.GiverReceiverInputInfo = {
const giver: GiverReceiverInputInfo = {
did: libsUtil.offerGiverDid(
this.veriClaim as GenericCredWrapper<OfferVerifiableCredential>,
),
};
console.log("giver & dialog", giver, this.$refs.customGiveDialog);
(this.$refs.customGiveDialog as GiftedDialog).open(
giver,
undefined,

View File

@@ -25,7 +25,7 @@
>
Do you agree?
</span>
<span v-else> Confirmation Details </span>
<span v-else> Details </span>
</h1>
</div>
@@ -65,11 +65,11 @@
<!-- Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4">
<div class="flex gap-4 overflow-hidden">
<div class="block flex gap-4 overflow-hidden">
<div class="overflow-hidden">
<div class="text-sm">
<div>
<fa icon="arrow-left" class="fa-fw text-slate-400" />
<fa icon="arrow-down" class="fa-fw text-slate-400" />
{{ giverName }}
</div>
<div class="ml-6">gave</div>
@@ -84,7 +84,7 @@
</div>
<div class="ml-6">to</div>
<div>
<fa icon="arrow-right" class="fa-fw text-slate-400" />
<fa icon="arrow-up" class="fa-fw text-slate-400" />
{{ recipientName }}
</div>
<div>
@@ -100,7 +100,7 @@
<router-link
:to="
'/project/' +
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '')
encodeURIComponent(giveDetails?.fulfillsPlanHandleId)
"
class="text-blue-500 mt-2 cursor-pointer"
target="_blank"
@@ -121,7 +121,7 @@
<router-link
:to="
'/claim/' +
encodeURIComponent(giveDetails?.fulfillsHandleId || '')
encodeURIComponent(giveDetails?.fulfillsHandleId)
"
class="text-blue-500 mt-2 cursor-pointer"
target="_blank"
@@ -129,7 +129,7 @@
This fulfills
{{
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
giveDetails?.fulfillsType || "",
giveDetails.fulfillsType,
)
}}
<fa icon="arrow-up-right-from-square" class="fa-fw" />
@@ -257,11 +257,10 @@
count as confirming it.
</div>
<div v-else-if="serverUtil.containsHiddenDid(veriClaim.claim)">
You cannot confirm this because some people are hidden.
You cannot confirm this because it contains hidden identifiers.
</div>
</div>
<!-- Note that a similar section is found in ClaimView.vue -->
<h2
class="font-bold uppercase text-xl text-blue-500 mt-8 mb-2 cursor-pointer"
@click="showDetails = !showDetails"
@@ -406,9 +405,10 @@ import { Router } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import * as serverUtil from "@/libs/endorserServer";
import { displayAmount, GiveSummaryRecord } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@@ -464,11 +464,12 @@ export default class ClaimView extends Vue {
async mounted() {
this.isLoading = true;
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
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;
this.isRegistered = settings?.isRegistered || false;
await accountsDB.open();
const accounts = accountsDB.accounts;
@@ -656,7 +657,7 @@ export default class ClaimView extends Vue {
}
if (this.giveDetails.fulfillsPlanHandleId) {
this.urlForNewGive +=
"&fulfillsProjectId=" +
"&projectId=" +
encodeURIComponent(this.giveDetails.fulfillsPlanHandleId);
}
@@ -763,7 +764,7 @@ export default class ClaimView extends Vue {
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation.",
text: "There was a problem submitting the confirmation. See logs for more info.",
},
5000,
);
@@ -843,7 +844,7 @@ export default class ClaimView extends Vue {
group: "alert",
type: "info",
title: "Cannot Confirm",
text: "You cannot confirm this because some people are hidden.",
text: "You cannot confirm this because it contains hidden identifiers.",
},
3000,
);

View File

@@ -112,8 +112,9 @@ import { Router } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
AgreeVerifiableCredential,
createEndorserJwtVcFromClaim,
@@ -143,12 +144,13 @@ export default class ContactAmountssView extends Vue {
async created() {
try {
await db.open();
const contactDid = (this.$route as Router).query["contactDid"] as string;
this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact);
@@ -278,7 +280,7 @@ export default class ContactAmountssView extends Vue {
(origClaim.object?.amountOfThisGood as number) || 1;
}
} catch (error) {
let userMessage = "There was an error.";
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.message) {

View File

@@ -11,7 +11,8 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
Given by...
Give to Contacts
</h1>
</div>
@@ -71,15 +72,15 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import GiftedDialog from "@/components/GiftedDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { GiverReceiverInputInfo } from "@/libs/util";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { GiverReceiverInputInfo } from "@/libs/endorserServer";
@Component({
components: { GiftedDialog, QuickNav, EntityIcon },
@@ -90,15 +91,14 @@ export default class ContactGiftingView extends Vue {
activeDid = "";
allContacts: Array<Contact> = [];
apiServer = "";
description = "";
projectId = "";
prompt = "";
projectId = localStorage.getItem("projectId") || "";
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
// .orderBy("name") wouldn't retrieve any entries with a blank name
// .toCollection.sortBy("name") didn't sort in an order I understood
@@ -107,9 +107,7 @@ export default class ContactGiftingView extends Vue {
(a.name || "").localeCompare(b.name || ""),
);
this.projectId = (this.$route as Router).query["projectId"] || "";
this.prompt = (this.$route as Router).query["prompt"] ?? this.prompt;
localStorage.removeItem("projectId");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
@@ -123,7 +121,7 @@ export default class ContactGiftingView extends Vue {
err.message ||
"There was an error retrieving your settings or contacts.",
},
5000,
-1,
);
}
}
@@ -137,7 +135,6 @@ export default class ContactGiftingView extends Vue {
recipient,
undefined,
"Given by " + (giver?.name || "someone not named"),
this.prompt,
);
}
}

View File

@@ -16,9 +16,9 @@
Contact Import
</h1>
<span class="flex justify-center">
<input type="checkbox" v-model="makeVisible" class="mr-2" />
Make my activity visible to these contacts.
<span>
Note that you will have to make them visible one-by-one in the list of
Contacts.
</span>
<div v-if="sameCount > 0">
<span v-if="sameCount == 1"
@@ -90,13 +90,12 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import OfferDialog from "@/components/OfferDialog.vue";
import { setVisibilityUtil } from "@/libs/endorserServer";
@Component({
components: { EntityIcon, OfferDialog, QuickNav },
@@ -108,8 +107,6 @@ export default class ContactImportView extends Vue {
libsUtil = libsUtil;
R = R;
activeDid = "";
apiServer = "";
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID
contactsImporting: Array<Contact> = []; // contacts from the import
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
@@ -118,19 +115,16 @@ export default class ContactImportView extends Vue {
Record<string, { new: string; old: string }>
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
importing = false;
makeVisible = true;
sameCount = 0;
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
// Retrieve the imported contacts from the query parameter
const importedContacts =
((this.$route as Router).query["contacts"] as string) || "[]";
this.contactsImporting = JSON.parse(importedContacts);
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
this.contactsSelected = new Array(this.contactsImporting.length).fill(
false,
);
await db.open();
const baseContacts = await db.contacts.toArray();
@@ -156,9 +150,9 @@ export default class ContactImportView extends Vue {
if (R.isEmpty(differences)) {
this.sameCount++;
}
// don't automatically import previous data
this.contactsSelected[i] = false;
} else {
// automatically import new data
this.contactsSelected[i] = true;
}
}
}
@@ -181,46 +175,13 @@ export default class ContactImportView extends Vue {
}
}
}
if (this.makeVisible) {
const failedVisibileToContacts = [];
for (let i = 0; i < this.contactsImporting.length; i++) {
const contact = this.contactsImporting[i];
if (contact) {
const visResult = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
db,
contact,
true,
);
if (!visResult.success) {
failedVisibileToContacts.push(contact);
}
}
}
if (failedVisibileToContacts.length) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Visibility Error",
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${
failedVisibileToContacts.length == 1 ? "" : "s"
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
},
-1,
);
}
}
this.importing = false;
this.$notify(
{
group: "alert",
type: "success",
title: "Imported",
title: "Import Success",
text:
`${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` +
(updatedCount ? ` ${updatedCount} updated.` : ""),

View File

@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw" />
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -34,8 +34,8 @@
click here to set it for them.
</span>
</p>
<UserNameDialog ref="userNameDialog" />
</div>
<UserNameDialog ref="userNameDialog" />
<div
@click="onCopyUrlToClipboard()"
@@ -90,6 +90,8 @@
<script lang="ts">
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import { sha256 } from "ethereum-cryptography/sha256.js";
import QRCodeVue3 from "qr-code-generator-vue3";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
@@ -99,11 +101,18 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import UserNameDialog from "@/components/UserNameDialog.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import {
deriveAddress,
getContactPayloadFromJwtUrl,
nextDerivationPath,
} from "@/libs/crypto";
import {
CONTACT_URL_PREFIX,
createEndorserJwtForDid,
ENDORSER_JWT_URL_LOCATION,
generateEndorserJwtForAccount,
isDid,
register,
@@ -132,28 +141,64 @@ export default class ContactQRScanShow extends Vue {
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.givenName = (settings?.firstName as string) || "";
this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact;
this.isRegistered = !!settings.isRegistered;
!!settings?.hideRegisterPromptOnNewContact;
this.isRegistered = !!settings?.isRegistered;
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (account) {
const publicKeyHex = account.publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const contactInfo = {
iat: Date.now(),
iss: this.activeDid,
own: {
name:
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""), // lastName is deprecated, pre v 0.1.3
publicEncKey,
profileImageUrl: settings?.profileImageUrl,
registered: settings?.isRegistered,
},
};
if (account?.mnemonic && account?.derivationPath) {
const newDerivPath = nextDerivationPath(
account.derivationPath as string,
);
const nextPublicHex = deriveAddress(
account.mnemonic as string,
newDerivPath,
)[2];
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
const nextPublicEncKeyHashBase64 =
Buffer.from(nextPublicEncKeyHash).toString("base64");
contactInfo.own.nextPublicEncKeyHash = nextPublicEncKeyHashBase64;
}
const vcJwt = await createEndorserJwtForDid(this.activeDid, contactInfo);
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
viewPrefix + vcJwt;
const name =
(settings.firstName || "") +
(settings.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // lastName is deprecated, pre v 0.1.3
this.qrValue = await generateEndorserJwtForAccount(
account,
!!settings.isRegistered,
!!settings?.isRegistered,
name,
settings.profileImageUrl,
false,
settings?.profileImageUrl as string,
);
}
}
@@ -362,7 +407,7 @@ export default class ContactQRScanShow extends Vue {
}
} catch (error) {
console.error("Error when registering:", error);
let userMessage = "There was an error.";
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.response?.data?.error?.message) {

View File

@@ -23,28 +23,20 @@
<!-- New Contact -->
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
<router-link
:to="{ name: 'invite-one' }"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<fa icon="envelope-open-text" class="fa-fw text-2xl" />
</router-link>
<router-link
:to="{ name: 'contact-qr' }"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
class="flex items-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-1 mr-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw text-2xl" />
</router-link>
<textarea
type="text"
placeholder="New URL or DID, Name, Public Key, Next Public Key Hash"
placeholder="URL or DID, Name, Public Key, Next Public Key Hash"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
v-model="contactInput"
/>
<button
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
@click="onClickNewContact()"
>
<fa icon="plus" class="fa-fw" />
@@ -87,9 +79,7 @@
class="text-md 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 py-1 rounded-md"
@click="toggleShowContactAmounts()"
>
{{
showGiveNumbers ? "Hide Hours, Offer, etc" : "See Hours, Offer, etc"
}}
{{ showGiveNumbers ? "Hide Given Hours" : "Show Given Hours" }}
</button>
</div>
</div>
@@ -168,12 +158,8 @@
}"
title="See more about this person"
>
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
<fa icon="circle-info" class="text-blue-500 ml-4" />
</router-link>
<span class="ml-4 text-sm overflow-hidden"
>{{ shortDid(contact.did) }}...</span
><!-- The first 18 characters of did:peer are the same. -->
</div>
<div id="ContactActions" class="flex gap-1.5 mt-2">
<div
@@ -182,25 +168,6 @@
>
<button
class="text-sm 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-1.5 rounded-l-md"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
:title="givenToMeDescriptions[contact.did] || ''"
>
From:
<br />
{{
/* eslint-disable prettier/prettier */
this.showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''"
>
@@ -216,12 +183,34 @@
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<br />
<fa icon="plus" />
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
@click="confirmShowGiftedDialog(contact.did, this.activeDid)"
:title="givenToMeDescriptions[contact.did] || ''"
>
From:
<br />
{{
/* eslint-disable prettier/prettier */
this.showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<br />
<fa icon="plus" />
</button>
<button
class="text-sm 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-1.5 rounded-md border border-blue-400"
@click="openOfferDialog(contact.did, contact.name)"
data-testId="offerButton"
>
Offer
</button>
@@ -273,7 +262,6 @@
<GiftedDialog ref="customGivenDialog" />
<OfferDialog ref="customOfferDialog" />
<ContactNameDialog ref="contactNameDialog" />
<div v-if="showLargeIdenticon" class="fixed z-[100] top-0 inset-x-0 w-full">
<div
@@ -294,51 +282,35 @@
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import { IndexableType } from "dexie";
import { JWTPayload } from "did-jwt";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue";
import ContactNameDialog from "@/components/ContactNameDialog.vue";
import TopMessage from "@/components/TopMessage.vue";
import { AppString, NotificationIface } from "@/constants/app";
import {
db,
retrieveSettingsForActiveAccount,
updateAccountSettings,
updateDefaultSettings,
} from "@/db/index";
import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import { decodeEndorserJwt } from "@/libs/crypto/vc";
import {
CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX,
GiverReceiverInputInfo,
GiveSummaryRecord,
getHeaders,
isDid,
register,
setVisibilityUtil,
UserInfo,
VerifiableCredential,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { generateSaveAndActivateIdentity } from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue";
import TopMessage from "@/components/TopMessage.vue";
@Component({
components: {
GiftedDialog,
EntityIcon,
OfferDialog,
QuickNav,
ContactNameDialog,
TopMessage,
},
components: { TopMessage, GiftedDialog, EntityIcon, OfferDialog, QuickNav },
})
export default class ContactsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -377,14 +349,14 @@ export default class ContactsView extends Vue {
public async created() {
await db.open();
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.isRegistered = !!settings?.isRegistered;
this.showGiveNumbers = !!settings.showContactGivesInline;
this.showGiveNumbers = !!settings?.showContactGivesInline;
this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact;
!!settings?.hideRegisterPromptOnNewContact;
if (this.showGiveNumbers) {
this.loadGives();
@@ -396,121 +368,6 @@ export default class ContactsView extends Vue {
this.contacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""),
);
// handle a contact sent via URL
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["contactJwt"] as string;
if (importedContactJwt) {
// really should fully verify contents
const { payload } = decodeEndorserJwt(importedContactJwt);
const userInfo = payload["own"] as UserInfo;
const newContact = {
did: payload["iss"],
name: userInfo.name,
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
profileImageUrl: userInfo.profileImageUrl,
publicKeyBase64: userInfo.publicEncKey,
registered: userInfo.registered,
} as Contact;
this.addContact(newContact);
}
// handle an invite JWT sent via URL
const importedInviteJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["inviteJwt"] as string;
if (importedInviteJwt === "") {
// this happens when a platform (usually iOS) doesn't include anything after the "=" in a shared link.
this.$notify(
{
group: "alert",
type: "warning",
title: "Blank Invite",
text: "The invite was not included. This can happen when your device cuts off the link, so you might try pasting the full link into a browser.",
},
7000,
);
} else if (importedInviteJwt) {
// make sure user is created
if (!this.activeDid) {
this.activeDid = await generateSaveAndActivateIdentity();
}
// send invite directly to server, with auth for this user
const headers = await getHeaders(this.activeDid);
try {
const response = await this.axios.post(
this.apiServer + "/api/v2/claim",
{ jwtEncoded: importedInviteJwt },
{ headers },
);
if (response.status != 201) {
throw { error: { response: response } };
}
await updateAccountSettings(this.activeDid, { isRegistered: true });
this.isRegistered = true;
this.$notify(
{
group: "alert",
type: "success",
title: "Registered",
text: "You are now registered.",
},
3000,
);
// now add the inviter as a contact
const payload: JWTPayload =
decodeEndorserJwt(importedInviteJwt).payload;
const registration = payload as VerifiableCredential;
(this.$refs.contactNameDialog as ContactNameDialog).open(
"Who Invited You?",
"",
(name) => {
// not doing await on purpose, so that they always see the onboarding
this.addContact({
did: registration.vc.credentialSubject.agent.identifier,
name: name,
registered: true,
});
this.showOnboardingInfo();
},
() => {
// not doing await on purpose, so that they always see the onboarding
this.addContact({
did: registration.vc.credentialSubject.agent.identifier,
name: "(person who invited you)",
registered: true,
});
this.showOnboardingInfo();
},
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error redeeming invite:", error);
let message = "Got an error sending the invite.";
if (
error.response &&
error.response.data &&
error.response.data.error
) {
if (error.response.data.error.message) {
message = error.response.data.error.message;
} else {
message = error.response.data.error;
}
} else if (error.message) {
message = error.message;
}
this.$notify(
{
group: "alert",
type: "danger",
title: "Error with Invite",
text: message,
},
5000,
);
}
}
}
private danger(message: string, title: string = "Error", timeout = 5000) {
@@ -525,21 +382,6 @@ export default class ContactsView extends Vue {
);
}
private showOnboardingInfo() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "They're Added To Your List",
text: "Would you like to go to the main page now?",
onYes: async () => {
(this.$router as Router).push({ name: "home" });
},
},
-1,
);
}
private filteredContacts() {
return this.showGiveNumbers
? this.contactsSelected.length === 0
@@ -595,7 +437,7 @@ export default class ContactsView extends Vue {
(useRecipient ? "given" : "received") +
" data from the server.",
},
5000,
-1,
);
}
};
@@ -868,17 +710,17 @@ export default class ContactsView extends Vue {
type: "confirm",
title: "Register",
text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => {
onCancel: async (stopAsking: boolean) => {
if (stopAsking) {
await updateDefaultSettings({
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onNo: async (stopAsking?: boolean) => {
onNo: async (stopAsking: boolean) => {
if (stopAsking) {
await updateDefaultSettings({
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
@@ -983,18 +825,11 @@ export default class ContactsView extends Vue {
}
} catch (error) {
console.error("Error when registering:", error);
let userMessage = "There was an error.";
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError.isAxiosError) {
if (
serverError.response?.data &&
typeof serverError.response.data === "object" &&
"error" in serverError.response.data &&
typeof serverError.response.data.error === "object" &&
serverError.response.data.error !== null &&
"message" in serverError.response.data.error
) {
userMessage = serverError.response.data.error.message as string;
if (serverError) {
if (serverError.response?.data?.error?.message) {
userMessage = serverError.response.data.error.message;
} else if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
@@ -1050,10 +885,7 @@ export default class ContactsView extends Vue {
}
return true;
} else {
console.error(
"Got strange result from setting visibility. It can happen when setting visibility on oneself.",
result,
);
console.error("Got strange result from setting visibility:", result);
const message =
(result.error as string) || "Could not set visibility on the server.";
this.$notify(
@@ -1069,6 +901,74 @@ export default class ContactsView extends Vue {
}
}
// note that this is also in DIDView.vue
private async checkVisibility(contact: Contact) {
const url =
this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
const headers = await getHeaders(this.activeDid);
if (!headers["Authorization"]) {
this.$notify(
{
group: "alert",
type: "danger",
title: "No Identity",
text: "There is no identity to use to check visibility.",
},
3000,
);
return;
}
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const visibility = resp.data;
contact.seesMe = visibility;
//console.log("Visi check:", visibility, contact.seesMe, contact.did);
await db.contacts.update(contact.did, { seesMe: visibility });
this.$notify(
{
group: "alert",
type: "info",
title: "Visibility Refreshed",
text:
libsUtil.nameForContact(contact, true) +
" can " +
(visibility ? "" : "not ") +
"see your activity.",
},
3000,
);
} else {
console.error("Got bad server response checking visibility:", resp);
const message = resp.data.error?.message || "Got bad server response.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Checking Visibility",
text: message,
},
5000,
);
}
} catch (err) {
console.error("Caught error from request to check visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Checking Visibility",
text: "Check connectivity and try again.",
},
3000,
);
}
}
private confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
// if they have unconfirmed amounts, ask to confirm those
if (
@@ -1110,8 +1010,7 @@ export default class ContactsView extends Vue {
}
private showGiftedDialog(giverDid: string, recipientDid: string) {
let giver: libsUtil.GiverReceiverInputInfo | undefined;
let receiver: libsUtil.GiverReceiverInputInfo | undefined;
let giver: GiverReceiverInputInfo, receiver: GiverReceiverInputInfo;
if (giverDid) {
giver = {
did: giverDid,
@@ -1134,7 +1033,7 @@ export default class ContactsView extends Vue {
newList[recipientDid] = (newList[recipientDid] || 0) + amount;
this.givenByMeUnconfirmed = newList;
};
customTitle = "Given to " + (receiver?.name || "Someone Unnamed");
customTitle = "Given to " + receiver.name;
} else {
// must be (recipientDid == this.activeDid)
callback = (amount: number) => {
@@ -1142,14 +1041,13 @@ export default class ContactsView extends Vue {
newList[giverDid] = (newList[giverDid] || 0) + amount;
this.givenToMeUnconfirmed = newList;
};
customTitle = "Received from " + (giver?.name || "Someone Unnamed");
customTitle = "Received from " + giver.name;
}
(this.$refs.customGivenDialog as GiftedDialog).open(
giver,
receiver,
undefined as unknown as string,
undefined as string,
customTitle,
undefined as unknown as string,
callback,
);
}
@@ -1164,7 +1062,8 @@ export default class ContactsView extends Vue {
private async toggleShowContactAmounts() {
const newShowValue = !this.showGiveNumbers;
try {
await updateDefaultSettings({
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: newShowValue,
});
} catch (err) {
@@ -1175,7 +1074,7 @@ export default class ContactsView extends Vue {
title: "Error Updating Contact Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
5000,
-1,
);
console.error(
"Telling user to try again after contact-amounts setting update because:",
@@ -1245,20 +1144,5 @@ export default class ContactsView extends Vue {
);
});
}
private shortDid(did: string) {
if (did.startsWith("did:peer:")) {
return (
did.substring(0, "did:peer:".length + 2) +
"..." +
did.substring("did:peer:".length + 18, "did:peer:".length + 25) +
"..."
);
} else if (did.startsWith("did:ethr:")) {
return did.substring(0, "did:ethr:".length + 9) + "...";
} else {
return did.substring(0, did.indexOf(":", 4) + 7) + "...";
}
}
}
</script>

View File

@@ -19,17 +19,14 @@
</div>
<!-- Identity Details -->
<div
v-if="!!contactFromDid"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"
>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div>
<h2 class="text-xl font-semibold">
{{ contactFromDid?.name || "(no name)" }}
{{ contact?.name || "(no name)" }}
<button
@click="
contactEdit = true;
contactNewName = (contactFromDid?.name as string) || '';
contactNewName = contact.name || '';
"
title="Edit"
>
@@ -41,8 +38,8 @@
class="ml-2 mr-2 mt-4"
>
Details
<fa v-if="showDidDetails" icon="chevron-down" class="text-blue-400" />
<fa v-else icon="chevron-right" class="text-blue-400" />
<fa v-if="showDidDetails" 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
@@ -52,15 +49,12 @@
>
</div>
<div class="flex justify-center mt-4">
<span
v-if="contactFromDid?.profileImageUrl"
class="flex justify-between"
>
<span v-if="contact?.profileImageUrl" class="flex justify-between">
<EntityIcon
:icon-size="96"
:profileImageUrl="contactFromDid?.profileImageUrl"
:profileImageUrl="contact?.profileImageUrl"
class="inline-block align-text-bottom border border-slate-300 rounded"
@click="showLargeIdenticonUrl = contactFromDid?.profileImageUrl"
@click="showLargeIdenticonUrl = contact?.profileImageUrl"
/>
</span>
</div>
@@ -69,60 +63,62 @@
<div v-if="activeDid" class="flex justify-between">
<div>
<button
v-if="
contactFromDid?.seesMe && contactFromDid.did !== activeDid
"
v-if="contact?.seesMe && contact.did !== activeDid"
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="confirmSetVisibility(contactFromDid, false)"
@click="confirmSetVisibility(contact, false)"
title="They can see you"
>
<fa icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
"
v-else-if="!contact?.seesMe && contact?.did !== activeDid"
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="confirmSetVisibility(contactFromDid, true)"
@click="confirmSetVisibility(contact, true)"
title="They cannot see you"
>
<fa icon="eye-slash" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="eye" class="text-white mx-2.5" />
<button
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="checkVisibility(contactFromDid)"
@click="checkVisibility(contact)"
title="Check Visibility"
v-if="contactFromDid?.did !== activeDid"
v-if="contact?.did !== activeDid"
>
<fa icon="rotate" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="rotate" class="text-white mx-2.5" />
</div>
<button
@click="confirmRegister(contactFromDid)"
@click="confirmRegister(contact)"
class="text-sm 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 ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
v-if="contactFromDid?.did !== activeDid"
v-if="contact?.did !== activeDid"
title="Registration"
>
<fa
v-if="contactFromDid?.registered"
v-if="contact?.registered"
icon="person-circle-check"
class="fa-fw"
/>
<fa v-else icon="person-circle-question" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="rotate" class="text-white ml-6 px-2.5" />
</div>
<button
@click="confirmDeleteContact(contactFromDid)"
@click="confirmDeleteContact(contact)"
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Delete"
>
<fa icon="trash-can" class="fa-fw" />
</button>
</div>
<div v-if="!contactFromDid?.profileImageUrl">
<div v-if="!contact?.profileImageUrl">
<div>Auto-Generated Icon</div>
<div class="flex justify-center">
<EntityIcon
@@ -154,16 +150,6 @@
</div>
</div>
</div>
<div v-else class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<!-- !contactFromDid -->
<div>
<h2 class="text-xl font-semibold">
{{ isMyDid ? "You" : "(no name)" }}
</h2>
</div>
</div>
<!-- Edit Name Dialog, maybe should be replaced by ContactNameDialog -->
<div v-if="contactEdit" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
@@ -200,9 +186,7 @@
</div>
<!-- Results List -->
<div v-if="claims.length > 0" class="mt-4">
<div class="text-l font-bold text-center">
Claims That Involve {{ isMyDid ? "You" : "Them" }}
</div>
<div class="text-l font-bold text-center">Claims That Involve Them</div>
</div>
<InfiniteScroll @reached-bottom="loadMoreData">
<ul>
@@ -238,8 +222,7 @@
v-if="!isLoading && claims.length === 0"
class="flex justify-center mt-4"
>
<span v-if="isMyDid">You have no claims yet.</span>
<span v-else>They are in no claims visible to you.</span>
<span>They are in no claims visible to you.</span>
</div>
</section>
</template>
@@ -254,9 +237,9 @@ import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { BoundingBox } from "@/db/tables/settings";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
capitalizeAndInsertSpacesBeforeCaps,
didInfoForContact,
@@ -287,15 +270,15 @@ export default class DIDView extends Vue {
yaml = yaml;
activeDid = "";
allMyDids: Array<string> = [];
apiServer = "";
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
contactFromDid?: Contact;
contact: Contact;
contactEdit = false;
contactNewName: string = "";
contactNewName?: string;
contactYaml = "";
hitEnd = false;
isLoading = false;
isMyDid = false;
searchBox: { name: string; bbox: BoundingBox } | null = null;
showDidDetails = false;
showLargeIdenticonId?: string;
@@ -307,28 +290,38 @@ export default class DIDView extends Vue {
displayAmount = displayAmount;
async mounted() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
const pathParam = window.location.pathname.substring("/did/".length);
let theContact: Contact | undefined;
if (pathParam) {
this.viewingDid = decodeURIComponent(pathParam);
this.contactFromDid = await db.contacts.get(this.viewingDid);
if (this.contactFromDid) {
this.contactYaml = yaml.dump(this.contactFromDid);
}
await this.loadClaimsAbout();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
for (const account of allAccounts) {
if (account.did === this.viewingDid) {
this.isMyDid = true;
break;
}
}
theContact = await db.contacts.get(this.viewingDid);
}
if (theContact) {
this.contact = theContact;
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "No valid claim ID was provided.",
},
-1,
);
return;
}
this.contactYaml = yaml.dump(this.contact);
await this.loadClaimsAbout();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
}
/**
@@ -343,20 +336,15 @@ export default class DIDView extends Vue {
// prompt with confirmation if they want to delete a contact
confirmDeleteContact(contact: Contact) {
let message =
"Are you sure you want to remove " +
libsUtil.nameForContact(contact, false) +
" from your contact list?";
if (contact.seesMe) {
message +=
" Note that they can see your activity, so if you want to hide your activity from them then you should do that first.";
}
this.$notify(
{
group: "modal",
type: "confirm",
title: "Delete",
text: message,
text:
"Are you sure you want to remove " +
libsUtil.nameForContact(contact, false) +
" from your contact list?",
onYes: async () => {
await this.deleteContact(contact);
},
@@ -389,7 +377,7 @@ export default class DIDView extends Vue {
title: "Register",
text:
"Are you sure you want to register " +
libsUtil.nameForContact(this.contactFromDid, false) +
libsUtil.nameForContact(this.contact, false) +
(contact.registered
? " -- especially since they are already marked as registered"
: "") +
@@ -442,7 +430,7 @@ export default class DIDView extends Vue {
}
} catch (error) {
console.error("Error when registering:", error);
let userMessage = "There was an error.";
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.response?.data?.error?.message) {
@@ -570,21 +558,9 @@ export default class DIDView extends Vue {
}
private async onClickSaveName(newName: string) {
if (!this.contactFromDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not A Contact",
text: "First add this on the contact page, then you can edit here.",
},
5000,
);
return;
}
this.contactFromDid.name = newName;
this.contact.name = newName;
return db.contacts
.update(this.contactFromDid.did, { name: newName })
.update(this.contact.did, { name: newName })
.then(() => (this.contactEdit = false));
}

View File

@@ -9,8 +9,6 @@
Discover Projects
</h1>
<OnboardingDialog ref="onboardingDialog" />
<!-- Quick Search -->
<div
id="QuickSearch"
@@ -78,12 +76,11 @@
</div>
<div v-if="isLocalActive">
<div class="text-center">
<div>
<button
class="ml-2 mt-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="$router.push({ name: 'search-area' })"
>
<fa icon="location-dot" class="fa-fw" />
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
</button>
</div>
@@ -96,15 +93,6 @@
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<div v-else-if="projects.length === 0" class="text-center mt-8">
<p class="text-lg text-slate-500">
<span v-if="isLocalActive">
<span v-if="searchBox"> None found in the selected area. </span>
<!-- Otherwise there's no search area selected so we'll just leave the search box for them to click. -->
</span>
<span v-else>No projects were found with that search.</span>
</p>
</div>
<!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData">
@@ -150,19 +138,16 @@ import { Router } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import OnboardingDialog from "@/components/OnboardingDialog.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { BoundingBox } from "@/db/tables/settings";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { didInfo, getHeaders, PlanData } from "@/libs/endorserServer";
import { OnboardPage } from "@/libs/util";
@Component({
components: {
InfiniteScroll,
OnboardingDialog,
ProjectIcon,
QuickNav,
TopMessage,
@@ -188,10 +173,11 @@ export default class DiscoverView extends Vue {
didInfo = didInfo;
async mounted() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = (settings.activeDid as string) || "";
this.apiServer = (settings.apiServer as string) || "";
this.searchBox = settings.searchBoxes?.[0] || null;
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.searchBox = settings?.searchBoxes?.[0] || null;
this.allContacts = await db.contacts.toArray();
@@ -201,12 +187,6 @@ export default class DiscoverView extends Vue {
this.searchTerms = (this.$route as Router).query["searchText"] || "";
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Discover,
);
}
if (this.searchBox) {
await this.searchLocal();
} else {
@@ -419,6 +399,7 @@ export default class DiscoverView extends Vue {
* @param id of the project
**/
onClickLoadProject(id: string) {
localStorage.setItem("projectId", id);
const route = {
path: "/project/" + encodeURIComponent(id),
};

View File

@@ -21,22 +21,12 @@
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1>
<h1 class="text-xl font-bold text-center mb-4">
<span>
From
{{
providedByProject
? providerProjectName
: providedByGiver
? giverName
: "someone not named"
}}
</span>
<br />
<span>From {{ giverName }}</span>
<span>
to
{{
givenToProject
? fulfillsProjectName
? projectName
: givenToRecipient
? recipientName
: "someone unidentified"
@@ -74,7 +64,7 @@
</div>
</div>
<div class="flex justify-center mt-4" data-testId="imagery">
<div class="flex justify-center mt-4" data-testid="imagery">
<span v-if="imageUrl" class="flex justify-between">
<a :href="imageUrl" target="_blank">
<img :src="imageUrl" class="h-24 rounded-xl" />
@@ -97,29 +87,7 @@
<div class="h-7 mt-4 flex">
<input
v-if="providerProjectId && !providedByGiver"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="providedByProject"
/>
<fa
v-else
icon="square"
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
@click="notifyUserOfProvidingProject()"
/>
<label class="text-sm mt-1">
{{
providerProjectId
? "This was provided by " + providerProjectName
: "This was not provided by a project"
}}
</label>
</div>
<div class="h-7 mt-4 flex">
<input
v-if="fulfillsProjectId && !givenToRecipient"
v-if="projectId && !givenToRecipient"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="givenToProject"
@@ -128,13 +96,13 @@
v-else
icon="square"
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
@click="notifyUserFulfillsProject()"
@click="notifyUserOfProject()"
/>
<label class="text-sm mt-1">
{{
fulfillsProjectId
? "This was given to " + fulfillsProjectName
: "No recipient project was chosen"
projectId
? "This was given to " + projectName
: "No project was chosen"
}}
</label>
</div>
@@ -166,7 +134,7 @@
<label class="text-sm mt-1">This was a trade (not a gift)</label>
</div>
<div v-if="showGeneralAdvanced" class="mt-4 flex">
<div class="mt-4 flex">
<router-link
:to="{
name: 'claim-add-raw',
@@ -213,7 +181,8 @@ import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import {
createAndSubmitGive,
didInfo,
@@ -243,10 +212,8 @@ export default class GiftedDetails extends Vue {
amountInput = "0";
description = "";
destinationPathAfter = "";
fulfillsProjectId = "";
fulfillsProjectName = "a project";
givenToProject = false; // basically static, based on input; if we allow changing then let's fix things (see below)
givenToRecipient = false; // basically static, based on input; if we allow changing then let's fix things (see below)
givenToProject = false;
givenToRecipient = false;
giverDid: string | undefined;
giverName = "";
hideBackButton = false;
@@ -255,13 +222,10 @@ export default class GiftedDetails extends Vue {
message = "";
offerId = "";
prevCredToEdit?: GenericCredWrapper<GiveVerifiableCredential>;
providerProjectId = "";
providerProjectName = "a project";
providedByProject = false; // basically static, based on input; if we allow changing then let's fix things (see below)
providedByGiver = false; // basically static, based on input; if we allow changing then let's fix things (see below)
projectId = "";
projectName = "a project";
recipientDid = "";
recipientName = "";
showGeneralAdvanced = false;
unitCode = "HUR";
libsUtil = libsUtil;
@@ -318,31 +282,11 @@ export default class GiftedDetails extends Vue {
offer?.identifier ||
this.offerId) as string;
// find any fulfills project ID
const fulfillsProject = fulfillsArray.find(
(rec) => rec["@type"] === "PlanAction",
);
// eslint-disable-next-line prettier/prettier
this.fulfillsProjectId =
((this.$route as Router).query["fulfillsProjectId"] ||
fulfillsProject?.identifier ||
this.fulfillsProjectId) as string;
// find any provider project ID
const provider = this.prevCredToEdit?.claim?.provider;
const providerArray = Array.isArray(provider)
? provider
: provider
? [provider]
: [];
const providerProject = providerArray.find(
(rec) => rec["@type"] === "PlanAction",
);
this.providerProjectId = ((this.$route as Router).query[
"providerProjectId"
] ||
providerProject?.identifier ||
this.providerProjectId) as string;
// find any project ID
const project = fulfillsArray.find((rec) => rec["@type"] === "PlanAction");
this.projectId = ((this.$route as Router).query["projectId"] ||
project?.identifier ||
this.projectId) as string;
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
@@ -374,70 +318,68 @@ export default class GiftedDetails extends Vue {
this.imageUrl = (this.$route as Router).query["shareUrl"] as string;
}
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
try {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
let allContacts: Contact[] = [];
let allMyDids: string[] = [];
if (
(this.giverDid && !this.giverName) ||
(this.recipientDid && !this.recipientName)
) {
allContacts = await db.contacts.toArray();
let allContacts: Contact[] = [];
let allMyDids: string[] = [];
if (
(this.giverDid && !this.giverName) ||
(this.recipientDid && !this.recipientName)
) {
allContacts = await db.contacts.toArray();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
allMyDids = allAccounts.map((acc) => acc.did);
if (this.giverDid && !this.giverName) {
this.giverName = didInfo(
this.giverDid,
this.activeDid,
allMyDids,
allContacts,
);
}
if (this.recipientDid && !this.recipientName) {
this.recipientName = didInfo(
this.recipientDid,
this.activeDid,
allMyDids,
allContacts,
);
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
allMyDids = allAccounts.map((acc) => acc.did);
if (this.giverDid && !this.giverName) {
this.giverName = didInfo(
this.giverDid,
this.activeDid,
allMyDids,
allContacts,
);
}
if (this.recipientDid && !this.recipientName) {
this.recipientName = didInfo(
this.recipientDid,
this.activeDid,
allMyDids,
allContacts,
);
}
}
// these should be functions but something's wrong with the syntax in the <> conditional
this.givenToProject = !!this.projectId;
this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
// 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,
);
}
// these should be functions but something's wrong with the syntax in the <> conditional
this.givenToProject = !!this.fulfillsProjectId;
this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
// these should be functions but something's wrong with the syntax in the <> conditional
this.providedByProject = !!this.providerProjectId;
this.providedByGiver = !this.providedByProject && !!this.giverDid;
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
if (this.fulfillsProjectId) {
// console.log("Getting project name from cache", this.fulfillsProjectId);
const fulfillsProject = await getPlanFromCache(
this.fulfillsProjectId,
if (this.projectId) {
// console.log("Getting project name from cache", this.projectId);
const project = await getPlanFromCache(
this.projectId,
this.axios,
this.apiServer,
this.activeDid,
);
this.fulfillsProjectName = fulfillsProject?.name
? `the project "${fulfillsProject.name}"`
: "a project";
}
if (this.providerProjectId) {
// console.log("Getting project name from cache", this.providerProjectId);
const providerProject = await getPlanFromCache(
this.providerProjectId,
this.axios,
this.apiServer,
this.activeDid,
);
this.providerProjectName = providerProject?.name
? `the project "${providerProject.name}"`
this.projectName = project?.name
? "the project: " + project.name
: "a project";
}
}
@@ -528,7 +470,7 @@ export default class GiftedDetails extends Vue {
console.error("Error deleting image:", error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).response.status === 404) {
console.log("Weird: the image was already deleted.", error);
console.log("The image was already deleted:", error);
localStorage.removeItem("imageUrl");
this.imageUrl = "";
@@ -602,35 +544,8 @@ export default class GiftedDetails extends Vue {
await this.recordGive();
}
notifyUserOfProvidingProject() {
// we're here because they clicked and either there is no provider project or there is a giver chosen
if (!this.providerProjectId) {
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: "To select a project as a provider, you must open this page through a project.",
},
3000,
);
} else {
// no providing project was chosen
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: "You cannot select both a giving project and person.",
},
3000,
);
}
}
notifyUserFulfillsProject() {
// we're here because they clicked and either there is no fulfills project or there is a recipient chosen
if (!this.fulfillsProjectId) {
notifyUserOfProject() {
if (!this.projectId) {
this.$notify(
{
group: "alert",
@@ -641,7 +556,7 @@ export default class GiftedDetails extends Vue {
3000,
);
} else {
// no fulfills project was chosen
// must be because givenToRecipient is true
this.$notify(
{
group: "alert",
@@ -691,9 +606,7 @@ export default class GiftedDetails extends Vue {
const recipientDid = this.givenToRecipient
? this.recipientDid
: undefined;
const fulfillsProjectId = this.givenToProject
? this.fulfillsProjectId
: undefined;
const projectId = this.givenToProject ? this.projectId : undefined;
let result;
if (this.prevCredToEdit) {
// don't create from a blank one in case some properties were set from a different interface
@@ -707,11 +620,10 @@ export default class GiftedDetails extends Vue {
this.description,
parseFloat(this.amountInput),
this.unitCode,
fulfillsProjectId,
projectId,
this.offerId,
this.isTrade,
this.imageUrl,
this.providerProjectId,
);
} else {
result = await createAndSubmitGive(
@@ -723,11 +635,10 @@ export default class GiftedDetails extends Vue {
this.description,
parseFloat(this.amountInput),
this.unitCode,
fulfillsProjectId,
projectId,
this.offerId,
this.isTrade,
this.imageUrl,
this.providerProjectId,
);
}
@@ -784,9 +695,7 @@ export default class GiftedDetails extends Vue {
constructGiveParam() {
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
const fulfillsProjectId = this.givenToProject
? this.fulfillsProjectId
: undefined;
const projectId = this.givenToProject ? this.projectId : undefined;
const giveClaim = hydrateGive(
this.prevCredToEdit?.claim as GiveVerifiableCredential,
this.giverDid,
@@ -794,11 +703,10 @@ export default class GiftedDetails extends Vue {
this.description,
parseFloat(this.amountInput),
this.unitCode,
fulfillsProjectId,
projectId,
this.offerId,
this.isTrade,
this.imageUrl,
this.providerProjectId,
this.prevCredToEdit?.id as string,
);
const claimStr = JSON.stringify(giveClaim);

View File

@@ -1,68 +0,0 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Notification Types
</h1>
</div>
<!-- eslint-disable prettier/prettier -->
<div>
<p>There are two types of notifications:</p>
<h2 class="text-xl font-semibold mt-4">Reminder Notifications</h2>
<div>
<p>
The Reminder Notification will be sent to you daily with a specific message,
at whatever time you choose. Use it to remind
yourself to act, for example: pause and consider who has given you
something, so you can record thanks in here.
</p>
<p>
This is a reliable message, but it doesn't contain any details about
activity that might be especially interesting to you.
</p>
</div>
<h2 class="text-xl font-semibold mt-4">New Activity Notifications</h2>
<div>
<p>
The New Activity Notification will be sent to you when there is new, relevant activity for you.
It will only trigger if something involves you or a project of interest; it will not
bug you for other, general activity.
</p>
<p>
This type is not as reliable as a Reminder Notification because mobile devices often suppress
such notifications to save battery. (We are working on other ways to notify you more
reliably. If you want to quickly check for relevant activity daily, use the Reminder
Notification and open the app and look for a large green button that points out new
activity that is personal to you.)
</p>
</div>
</div>
<!-- eslint-enable -->
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class HelpNotificationTypesView extends Vue {}
</script>

View File

@@ -39,15 +39,6 @@
</p>
</div>
<h2 class="text-xl font-semibold mt-4">Android Users</h2>
<div>
<p>
Note that you may not receive notifications when the app is in the
background. When you're done working, close the app, and then you'll
get the reminder notifications.
</p>
</div>
<h2 class="text-xl font-semibold mt-4">
If this app doesn't support notifications...
<!-- Note that that exact verbiage shows in a message elsewhere. -->
@@ -314,8 +305,8 @@ export default class HelpNotificationsView extends Vue {
async mounted() {
try {
const registration = await navigator.serviceWorker?.ready;
const fullSub = await registration?.pushManager.getSubscription();
const registration = await navigator.serviceWorker.ready;
const fullSub = await registration.pushManager.getSubscription();
this.subscriptionJSON = fullSub?.toJSON();
} catch (error) {
console.error("Mount error:", error);
@@ -375,7 +366,7 @@ export default class HelpNotificationsView extends Vue {
showTestNotification() {
const TEST_NOTIFICATION_TITLE = "It Worked";
navigator.serviceWorker?.ready
navigator.serviceWorker.ready
.then((registration) => {
return registration.showNotification(TEST_NOTIFICATION_TITLE, {
body: "This is your test notification.",

View File

@@ -12,91 +12,45 @@
</h1>
</div>
<p>
To invite someone the easiest way, send them a link that you generate from
this page:
<router-link
:to="{ name: 'invite-one' }"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
>
<fa icon="envelope-open-text" class="fa-fw text-xl"
/></router-link>
</p>
<p>Then watch that page to see when they accept their invite.</p>
<p>
(That page is also reachable from the Contacts <fa icon="users" /> page
though the invitation <fa icon="envelope-open-text" /> icon.)
</p>
<h1 class="mt-4 font-bold text-xl">Next Steps</h1>
Although not totally necessary, backups are important to understand.
<div class="ml-4">
<h1 class="font-bold text-xl">Without a backup, you can lose data.</h1>
<div>
<p>
Exporting backups (from the Account <fa icon="circle-user" /> screen)
is important for the case where they lose their device. This is
especially true for the Identifier Seed: that is theirs and and theirs
alone, and currently nobody else can recover it if they lose it. The
good thing is that anyone can create a new account and simply inform
their network of their new ID.
</p>
</div>
</div>
<h1 class="mt-4 font-bold text-xl">Advanced</h1>
The following are optional steps for even more functionality.
<!-- eslint-disable prettier/prettier -->
<div class="ml-4">
<h1 class="font-bold text-xl">Add Contact & Register</h1>
<p>
You share even more information such as your picture and name when
you share with your QR code at these links: <fa icon="qrcode" />
</p>
<p>
Scanning
those with your cameras will automatically register people and add them
to each other's contact lists.
</p>
<p>
The following are more detailed manual steps:
</p>
<div>
<p>
1) Have them follow their yellow prompts.
</p>
<p>
2) Scan their QR, or have them tap on it to copy their info and send it to you.
Then you can add them to your Contacts <fa icon="users" />
</p>
<p>
3) You can register them at their info page <fa icon="circle-info" />
and click on the register button <fa icon="person-circle-question" />
</p>
<p>
4) Add yourself to their Contacts <fa icon="users" />
</p>
</div>
<h1 class="font-bold text-xl">Install</h1>
<div>
<p>
Have them visit TimeSafari.app in a browser, preferably Chrome or Safari,
and then look for the "Install" selection which adds this app to their desktop.
This enables other things, like the ability to "share" a photo from their
device directly to Time Safari, and it makes notifications more reliable.
1) Have them visit TimeSafari.app in a browser, preferably Chrome or Safari.
</p>
<p>
2) Have them "Install" the site to their desktop.
</p>
</div>
<h1 class="font-bold text-xl">Add Contact & Register</h1>
<div>
<p>
3) Have them follow their yellow prompts.
</p>
<p>
4) Add them to your contacts <fa icon="users" />
</p>
<p>
5) Register them <fa icon="person-circle-question" />
</p>
<p>
6) Add yourself to their contacts <fa icon="users" />
</p>
</div>
<h1 class="font-bold text-xl">Enable Notifications</h1>
<div>
<p>
Enable notifications from the Account page <fa icon="circle-user" />.
Those notifications might show up on the device depending on your settings.
For the most reliable habits, people should own alarm or some other ritual to look every day.
7) Enable notifications from <fa icon="circle-user" />
</p>
</div>
<h1 class="font-bold text-xl">Discuss Backups</h1>
<div>
<p>
8) Exporting backups <fa icon="circle-user" /> are important if they lose their phone --- especially for the Identifier Seed!
</p>
</div>

View File

@@ -21,20 +21,12 @@
</h1>
</div>
<!-- eslint-disable prettier/prettier max-len -->
<!-- eslint-disable prettier/prettier -->
<div>
<p>
This app focuses on gifts & gratitude, using them to build cool things together with your network.
</p>
<p class="ml-4">
If you'd like to see the page-by-page help,
<span
@click="unsetFinishedOnboarding()"
class="text-blue-500 cursor-pointer"
>click here</span>.
</p>
<h2 class="text-xl font-semibold">What is the idea here?</h2>
<p>
We are building networks of people who want to grow good society from the ground up, using modern
@@ -387,7 +379,7 @@
<fa icon="circle-user" /> page.
</p>
<p>
There is even more functionality in a mobile app (and more
There is a even more functionality in a mobile app (and more
documentation) at
<a href="https://endorser.ch" target="_blank" class="text-blue-500">
EndorserSearch.com
@@ -523,8 +515,6 @@
<fa v-show="!showDidCopy" icon="copy" class="text-slate-400 fa-fw" />
</button>
<span v-show="showDidCopy" class="ml-2 text-sm text-green-500">Copied</span>
You can donate online via
<a href="https://www.patreon.com/TimeSafari" target="_blank" class="text-blue-500">Patreon here</a>.
For other donations, contact us.
</p>
@@ -556,16 +546,11 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import * as Package from "../../package.json";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import {
retrieveSettingsForActiveAccount,
updateAccountSettings,
} from "@/db/index";
@Component({ components: { QuickNav } })
export default class Help extends Vue {
@@ -588,15 +573,5 @@ export default class Help extends Vue {
.copy(text)
.then(() => setTimeout(fn, 2000));
}
async unsetFinishedOnboarding() {
const settings = await retrieveSettingsForActiveAccount();
if (settings.activeDid) {
await updateAccountSettings(settings.activeDid || "", {
finishedOnboarding: false,
});
}
(this.$router as Router).push({ name: "home" });
}
}
</script>

View File

@@ -8,8 +8,6 @@
{{ AppString.APP_NAME }}
</h1>
<OnboardingDialog ref="onboardingDialog" />
<!-- prompt to install notifications with notificationsSupported, which we're making an advanced feature -->
<div class="mb-8 mt-8">
<div
@@ -86,11 +84,11 @@
id="noticeSomeoneMustRegisterYou"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<!-- !isCreatingIdentifier && !isRegistered -->
<!-- activeDid && !isRegistered -->
To share, someone must register you.
<div class="block text-center">
<button
@click="showNameThenIdDialog()"
@click="showNameDialog()"
class="text-md font-bold 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-2 py-3 rounded-md"
>
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
@@ -109,17 +107,19 @@
</div>
<div v-else id="sectionRecordSomethingGiven">
<!-- !isCreatingIdentifier && isRegistered -->
<!-- activeDid && isRegistered -->
<!-- show the actions for recognizing a give -->
<div class="flex">
<h2 class="text-xl font-bold">What have you seen someone do?</h2>
<button
@click="openGiftedPrompts()"
class="ml-2 block text-xs 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 rounded-md"
>
<fa icon="lightbulb" class="fa-fw" />
</button>
<div class="flex justify-between">
<h2 class="text-xl font-bold">Record Something Given By:</h2>
<div class="flex justify-end">
<button
@click="openGiftedPrompts()"
class="block text-center text-md 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-4 py-2 rounded-md"
>
Ideas...
</button>
</div>
</div>
<ul
@@ -136,9 +136,6 @@
Unnamed/Unknown
</h3>
</li>
<li v-if="allContacts.length === 0" class="text-sm">
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 6)"
:key="contact.did"
@@ -159,9 +156,9 @@
<router-link
v-if="allContacts.length >= 6"
:to="{ name: 'contact-gift' }"
class="flex align-bottom text-xs text-blue-500 mt-12"
class="block text-center text-md font-bold 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-2 py-3 rounded-md"
>
... or someone else...
Choose From All Contacts
</router-link>
</li>
</ul>
@@ -175,68 +172,26 @@
<FeedFilters ref="feedFilters" />
<!-- Results List -->
<div class="bg-slate-100 rounded-md px-4 py-3 mt-4 mb-4">
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4 mb-4">
<div class="flex items-center mb-4">
<h2 class="text-xl font-bold">
Latest Activity
<button @click="openFeedFilters()">
<span class="text-xs text-white">
<fa
v-if="resultsAreFiltered()"
icon="filter"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md"
/>
<fa
v-else
icon="filter"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md"
/>
</span>
</button>
</h2>
</div>
<div
@click="goToActivityToUserPage()"
class="border-t p-2 border-slate-300"
>
<div class="flex justify-center">
<div
v-if="numNewOffersToUser"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white"
>
<h2 class="text-xl font-bold">Latest Activity</h2>
<button @click="openFeedFilters()" class="block text-center ml-auto">
<span class="text-sm text-white">
<span
class="block text-center text-6xl"
data-testId="newDirectOffersActivityNumber"
v-if="resultsAreFiltered()"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
>
{{ numNewOffersToUser }}{{ newOffersToUserHitLimit ? "+" : "" }}
Filtered
</span>
<p class="text-center">
new offer{{ numNewOffersToUser === 1 ? "" : "s" }} to you
</p>
</div>
<div
v-if="numNewOffersToUserProjects"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white"
>
<span
class="block text-center text-6xl"
data-testId="newOffersToUserProjectsActivityNumber"
v-else
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
>
{{ numNewOffersToUserProjects
}}{{ newOffersToUserProjectsHitLimit ? "+" : "" }}
Unfiltered
</span>
<p class="text-center">
new offer{{ numNewOffersToUserProjects === 1 ? "" : "s" }} to your
projects
</p>
</div>
</div>
<div class="flex justify-end mt-2">
<button class="text-blue-500">View All New Activity For You</button>
</div>
</span>
</button>
</div>
<InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="border-t border-slate-300">
<li
@@ -245,7 +200,7 @@
:key="record.jwtId"
>
<div
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold text-sm"
v-if="record.jwtId == feedLastViewedClaimId"
>
You've already seen all the following
@@ -310,7 +265,7 @@
<a @click="onClickLoadClaim(record.jwtId)">
<fa
icon="file-lines"
class="pl-2 text-slate-500 cursor-pointer"
class="pl-2 text-blue-500 cursor-pointer"
/>
</a>
</span>
@@ -324,15 +279,6 @@
>
<fa icon="hammer" class="text-blue-500" />
</router-link>
<router-link
v-if="record.providerPlanHandleId"
:to="
'/project/' +
encodeURIComponent(record.providerPlanHandleId)
"
>
<fa icon="hammer" class="text-blue-500" />
</router-link>
</span>
</div>
<div v-if="record.image" class="flex justify-center">
@@ -368,7 +314,6 @@ import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPrompts from "@/components/GiftedPrompts.vue";
import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import OnboardingDialog from "@/components/OnboardingDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import UserNameDialog from "@/components/UserNameDialog.vue";
@@ -377,17 +322,13 @@ import {
NotificationIface,
PASSKEYS_ENABLED,
} from "@/constants/app";
import {
accountsDB,
db,
retrieveSettingsForActiveAccount,
updateAccountSettings,
} from "@/db/index";
import { db, accountsDB } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import {
BoundingBox,
checkIsAnyFeedFilterOn,
isAnyFeedFilterOn,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import {
contactForDid,
@@ -395,15 +336,12 @@ import {
didInfoForContact,
fetchEndorserRateLimits,
getHeaders,
getNewOffersToUser,
getNewOffersToUserProjects,
getPlanFromCache,
GiverReceiverInputInfo,
GiveSummaryRecord,
} from "@/libs/endorserServer";
import {
generateSaveAndActivateIdentity,
GiverReceiverInputInfo,
OnboardPage,
registerSaveAndActivatePasskey,
} from "@/libs/util";
@@ -414,7 +352,6 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
profileImageUrl?: string;
};
image?: string;
providerPlanName?: string;
recipientProjectName?: string;
receiver: {
displayName: string;
@@ -430,13 +367,12 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
},
},
components: {
EntityIcon,
FeedFilters,
GiftedDialog,
GiftedPrompts,
InfiniteScroll,
OnboardingDialog,
FeedFilters,
QuickNav,
EntityIcon,
InfiniteScroll,
TopMessage,
UserNameDialog,
},
@@ -461,12 +397,6 @@ export default class HomeView extends Vue {
isFeedFilteredByNearby = false;
isFeedLoading = true;
isRegistered = false;
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
newOffersToUserHitLimit: boolean = false;
newOffersToUserProjectsHitLimit: boolean = false;
numNewOffersToUser: number = 0; // number of new offers-to-user
numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
@@ -487,28 +417,21 @@ export default class HomeView extends Vue {
this.allMyDids = [newDid];
}
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
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();
this.feedLastViewedClaimId = settings.lastViewedClaimId;
this.givenName = settings.firstName || "";
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isRegistered = !!settings.isRegistered;
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId;
this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc;
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.givenName = settings?.firstName || "";
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isRegistered = !!settings?.isRegistered;
this.searchBoxes = settings?.searchBoxes || [];
this.showShortcutBvc = !!settings?.showShortcutBvc;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Home,
);
}
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
console.log("getting through mounted");
// someone may have have registered after sharing contact info, so recheck
if (!this.isRegistered && this.activeDid) {
@@ -519,7 +442,9 @@ export default class HomeView extends Vue {
this.activeDid,
);
if (resp.status === 200) {
await updateAccountSettings(this.activeDid, {
// we just needed to know that they're registered
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: true,
});
this.isRegistered = true;
@@ -532,28 +457,6 @@ export default class HomeView extends Vue {
// this returns a Promise but we don't need to wait for it
this.updateAllFeed();
if (this.activeDid) {
const offersToUserData = await getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserJwtId,
);
this.numNewOffersToUser = offersToUserData.data.length;
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
}
if (this.activeDid) {
const offersToUserProjects = await getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
);
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings or feed.", err);
@@ -590,10 +493,11 @@ export default class HomeView extends Vue {
// only called when a setting was changed
async reloadFeedOnChange() {
const settings = await retrieveSettingsForActiveAccount();
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
this.feedData = [];
this.feedPreviousOldestId = undefined;
@@ -629,6 +533,7 @@ export default class HomeView extends Vue {
async updateAllFeed() {
this.isFeedLoading = true;
let endOfResults = true;
console.log("about to retrieveGives");
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
@@ -648,7 +553,7 @@ export default class HomeView extends Vue {
// This has indeed proven problematic. See loadMoreGives
// We should display it immediately and then get the plan later.
const fulfillsPlan = await getPlanFromCache(
const plan = await getPlanFromCache(
record.fulfillsPlanHandleId,
this.axios,
this.apiServer,
@@ -664,13 +569,8 @@ export default class HomeView extends Vue {
if (!anyMatch && this.isFeedFilteredByNearby) {
// check if the associated project has a location inside user's search box
if (record.fulfillsPlanHandleId) {
if (fulfillsPlan?.locLat && fulfillsPlan?.locLon) {
if (
this.latLongInAnySearchBox(
fulfillsPlan.locLat,
fulfillsPlan.locLon,
)
) {
if (plan?.locLat && plan?.locLon) {
if (this.latLongInAnySearchBox(plan.locLat, plan.locLon)) {
anyMatch = true;
}
}
@@ -680,17 +580,6 @@ export default class HomeView extends Vue {
continue;
}
// checking for arrays due to legacy data
const provider = Array.isArray(claim.provider)
? claim.provider[0]
: claim.provider;
const providedByPlan = await getPlanFromCache(
provider?.identifier as string,
this.axios,
this.apiServer,
this.activeDid,
);
const newRecord: GiveRecordWithContactInfo = {
...record,
giver: didInfoForContact(
@@ -700,9 +589,7 @@ export default class HomeView extends Vue {
this.allMyDids,
),
image: claim.image,
providerPlanHandleId: provider?.identifier as string,
providerPlanName: providedByPlan?.name as string,
recipientProjectName: fulfillsPlan?.name as string,
recipientProjectName: plan?.name as string,
receiver: didInfoForContact(
recipientDid,
this.activeDid,
@@ -796,70 +683,50 @@ export default class HomeView extends Vue {
}
/**
* Only show giver and/or receiver info first if they're named in your contacts.
* Only show giver and/or receiver info first if they're named.
* - If only giver is named, show "... gave"
* - If only receiver is named, show "... received"
*/
const giverInfo = giveRecord.giver;
const recipientInfo = giveRecord.receiver;
// any specific names should be shown first
if (giverInfo.known && recipientInfo.known) {
// both giver and recipient are named
return `${giverInfo.displayName} gave to ${recipientInfo.displayName}: ${gaveAmount}`;
} else if (giverInfo.known) {
// giver is known but recipient is not
// giver is named but recipient is not
// show the project name if to one
if (giveRecord.recipientProjectName) {
return `${giverInfo.displayName} gave: ${gaveAmount} (to the project "${giveRecord.recipientProjectName}")`;
} else {
// it's not to a project
return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`;
// retrieve the project name
return `${giverInfo.displayName} gave: ${gaveAmount} (to the project ${giveRecord.recipientProjectName})`;
}
} else if (recipientInfo.known) {
// recipient is known but giver is not
// show the project name if from one
if (giveRecord.providerPlanName) {
return `${recipientInfo.displayName} received: ${gaveAmount} (from the project "${giveRecord.providerPlanName}")`;
} else {
// it's not from a project
return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`;
}
// it's not to a project
return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`;
} else if (recipientInfo.known) {
// recipient is named but giver is not
return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`;
} else {
// neither giver nor recipient are named
// create the part in parens
let peopleInfo = "";
if (giveRecord.providerPlanName || giveRecord.recipientProjectName) {
if (giveRecord.providerPlanName) {
peopleInfo = `from the project "${giveRecord.providerPlanName}"`;
} else {
peopleInfo = `from ${giverInfo.displayName}`;
}
if (giveRecord.recipientProjectName) {
peopleInfo += ` to the project "${giveRecord.recipientProjectName}"`;
} else {
peopleInfo += ` to ${recipientInfo.displayName}`;
}
} else {
if (giverInfo.displayName === recipientInfo.displayName) {
peopleInfo = `between two who are ${giverInfo.displayName}`;
} else {
peopleInfo = `from ${giverInfo.displayName} to ${recipientInfo.displayName}`;
}
// show the project name if to one
if (giveRecord.recipientProjectName) {
// retrieve the project name
return `${gaveAmount} (to the project ${giveRecord.recipientProjectName})`;
}
// it's not to a project
let peopleInfo;
if (giverInfo.displayName === recipientInfo.displayName) {
peopleInfo = `between two who are ${giverInfo.displayName}`;
} else {
peopleInfo = `from ${giverInfo.displayName} to ${recipientInfo.displayName}`;
}
return gaveAmount + " (" + peopleInfo + ")";
}
}
goToActivityToUserPage() {
(this.$router as Router).push({ name: "new-activity" });
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
@@ -875,30 +742,27 @@ export default class HomeView extends Vue {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
openDialog(giver?: GiverReceiverInputInfo, description?: string) {
openDialog(giver?: GiverReceiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "you",
} as GiverReceiverInputInfo,
},
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
}
openGiftedPrompts() {
(this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) =>
this.openDialog(giver as GiverReceiverInputInfo, description),
);
(this.$refs.giftedPrompts as GiftedPrompts).open();
}
openFeedFilters() {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
}
toastUser(message: string) {
toastUser(message) {
this.$notify(
{
group: "alert",
@@ -914,7 +778,7 @@ export default class HomeView extends Vue {
return known ? "text-slate-500" : "text-slate-100";
}
showNameThenIdDialog() {
showNameDialog() {
if (!this.givenName) {
(this.$refs.userNameDialog as UserNameDialog).open(() => {
this.promptForShareMethod();

View File

@@ -102,8 +102,8 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { NotificationIface } from "@/constants/app";
import { db, accountsDB, retrieveSettingsForActiveAccount } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } })
@@ -118,10 +118,11 @@ export default class IdentitySwitcherView extends Vue {
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.apiServerInput = settings.apiServer || "";
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();

View File

@@ -53,13 +53,6 @@
<input type="checkbox" class="mr-2" v-model="shouldErase" />
<label>Erase the previous identifier.</label>
</div>
<div v-if="isNotProdServer()" class="mt-4 text-blue-500">
<!-- if they click this, fill in the mnemonic seed-input with the test mnemonic -->
<button @click="mnemonic = TEST_USER_0_MNEMONIC">
Use mnemonic for Test User #0
</button>
</div>
</div>
<div class="mt-8">
@@ -86,8 +79,8 @@
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
DEFAULT_ROOT_DERIVATION_PATH,
@@ -99,40 +92,28 @@ import {
components: {},
})
export default class ImportAccountView extends Vue {
TEST_USER_0_MNEMONIC =
"rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage";
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
AppString = AppString;
$notify!: (notification: NotificationIface, timeout?: number) => void;
apiServer = "";
address = "";
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
mnemonic = "";
address = "";
numAccounts = 0;
privateHex = "";
publicHex = "";
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
showAdvanced = false;
shouldErase = false;
async created() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
// get the server, to help with import on the test server
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
}
public onCancelClick() {
(this.$router as Router).back();
}
public isNotProdServer() {
return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER;
}
public async fromMnemonic() {
const mne: string = this.mnemonic.trim().toLowerCase();
try {

View File

@@ -1,392 +0,0 @@
<template>
<QuickNav selected="Invite" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 class="text-4xl text-center font-light">Invitations</h1>
<ul class="ml-8 mt-4 list-outside list-disc w-5/6">
<li>
Note when sending
<span
v-if="!showAppleWarning"
class="text-blue-500 cursor-pointer"
@click="showAppleWarning = !showAppleWarning"
>
to Apple users...
</span>
<span v-else>
to Apple users: their links often fail because their device cuts off
part of the link. You might need to send it to them some other way,
like in an email.
</span>
</li>
</ul>
<!-- New Project -->
<button
v-if="isRegistered"
class="fixed right-6 top-12 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="createInvite()"
>
<fa icon="plus" class="fa-fw"></fa>
</button>
<InviteDialog ref="inviteDialog" />
<!-- Invites Table -->
<div v-if="invites.length" class="mt-6">
<table class="min-w-full bg-white">
<thead>
<tr>
<th class="py-2">
ID
<br />
(click for link)
</th>
<th class="py-2">Notes</th>
<th class="py-2">Expires At</th>
<th class="py-2">Redeemed</th>
</tr>
</thead>
<tbody>
<tr
v-for="invite in invites"
:key="invite.inviteIdentifier"
class="border-t py-2"
>
<td>
<span
v-if="
!invite.redeemedAt &&
invite.expiresAt > new Date().toISOString()
"
@click="
copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)
"
class="text-center text-blue-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
>
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
</span>
<span
v-else
@click="
showInvite(
invite.inviteIdentifier,
!!invite.redeemedAt,
invite.expiresAt < new Date().toISOString(),
)
"
class="text-center text-slate-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
>
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
</span>
</td>
<td class="text-left" :data-testId="inviteLink(invite.jwt)">
{{ invite.notes }}
</td>
<td class="text-center">
{{ invite.expiresAt.substring(0, 10) }}
</td>
<td class="text-center">
{{ invite.redeemedAt?.substring(0, 10) }}
<br />
{{ getTruncatedRedeemedBy(invite.redeemedBy) }}
<br />
<fa
v-if="invite.redeemedBy && !contactsRedeemed[invite.redeemedBy]"
icon="plus"
class="bg-green-600 text-white px-1 py-1 rounded-full cursor-pointer"
@click="addNewContact(invite.redeemedBy)"
/>
</td>
<td>
<fa
icon="trash-can"
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
@click="deleteInvite(invite.inviteIdentifier, invite.notes)"
/>
</td>
</tr>
</tbody>
</table>
<ContactNameDialog ref="contactNameDialog" />
</div>
<p v-else class="mt-6 text-center">No invites found.</p>
</section>
</template>
<script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import ContactNameDialog from "@/components/ContactNameDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import InviteDialog from "@/components/InviteDialog.vue";
import { APP_SERVER, NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db";
import { createInviteJwt, getHeaders } from "@/libs/endorserServer";
interface Invite {
inviteIdentifier: string;
expiresAt: string;
jwt: string;
notes: string;
redeemedAt: string | null;
redeemedBy: string | null;
}
@Component({
components: { ContactNameDialog, QuickNav, TopMessage, InviteDialog },
})
export default class InviteOneView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
invites: Invite[] = [];
activeDid: string = "";
apiServer: string = "";
contactsRedeemed = {};
isRegistered: boolean = false;
showAppleWarning = false;
async mounted() {
try {
await db.open();
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
const headers = await getHeaders(this.activeDid);
const response = await axios.get(
this.apiServer + "/api/userUtil/invite",
{ headers },
);
this.invites = response.data.data;
const baseContacts = await db.contacts.toArray();
for (const invite of this.invites) {
const contact = baseContacts.find(
(contact) => contact.did === invite.redeemedBy,
);
if (contact) {
this.contactsRedeemed[invite.redeemedBy] = contact;
}
}
} catch (error) {
console.error("Error fetching invites:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Load Error",
text: "Got an error loading your invites.",
},
5000,
);
}
}
getTruncatedInviteId(inviteId: string): string {
if (inviteId.length <= 9) return inviteId;
return `${inviteId.slice(0, 6)}...`;
}
getTruncatedRedeemedBy(redeemedBy: string | null): string {
if (!redeemedBy) return "";
if (this.contactsRedeemed[redeemedBy]) {
return this.contactsRedeemed[redeemedBy].name;
}
if (redeemedBy.length <= 19) return redeemedBy;
return `${redeemedBy.slice(0, 13)}...${redeemedBy.slice(-3)}`;
}
inviteLink(jwt: string): string {
return APP_SERVER + "/contacts?inviteJwt=" + jwt;
}
copyInviteAndNotify(inviteId: string, jwt: string) {
useClipboard().copy(this.inviteLink(jwt));
this.$notify(
{
group: "alert",
type: "success",
title: "Copied",
text: "Your clipboard now contains the link for invite " + inviteId,
},
5000,
);
}
showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
let message = `Your clipboard now contains the invite ID ${inviteId}`;
if (redeemed) {
message += " (This invite has been used.)";
} else if (expired) {
message += " (This invite has expired.)";
}
useClipboard().copy(inviteId);
this.$notify(
{
group: "alert",
type: "success",
title: "Copied",
text: message,
},
5000,
);
}
lookForErrorAndNotify(error, title: string, defaultMessage: string) {
console.error(title, "-", error);
let message = defaultMessage;
if (error.response && error.response.data && error.response.data.error) {
if (error.response.data.error.message) {
message = error.response.data.error.message;
} else {
message = error.response.data.error;
}
}
this.$notify(
{
group: "alert",
type: "danger",
title: title,
text: message,
},
5000,
);
}
async createInvite() {
const inviteIdentifier =
Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2);
(this.$refs.inviteDialog as InviteDialog).open(
inviteIdentifier,
async (notes, expiresAt) => {
try {
const headers = await getHeaders(this.activeDid);
if (!expiresAt) {
throw {
response: {
data: { error: "You must select an expiration date." },
},
};
}
const expiresIn = (new Date(expiresAt).getTime() - Date.now()) / 1000;
const inviteJwt = await createInviteJwt(
this.activeDid,
undefined,
inviteIdentifier,
expiresIn,
);
await axios.post(
this.apiServer + "/api/userUtil/invite",
{ inviteJwt: inviteJwt, notes: notes },
{ headers },
);
this.invites.push({
inviteIdentifier: inviteIdentifier,
expiresAt: expiresAt,
jwt: inviteJwt,
notes: notes,
redeemedAt: null,
redeemedBy: null,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
this.lookForErrorAndNotify(
error,
"Error Creating Invite",
"Got an error creating your invite.",
);
}
},
);
}
addNewContact(did) {
(this.$refs.contactNameDialog as ContactNameDialog).open(
"Who Sent You The Invite?",
"Their name will be added to your contact list.",
(name) => {
// the person obviously registered themselves and this user already granted visibility, so we just add them
const contact = {
did: did,
name: name,
registered: true,
};
db.contacts.add(contact);
this.contactsRedeemed[did] = contact;
this.$notify(
{
group: "alert",
type: "success",
title: "Contact Added",
text: `${name} has been added to your contacts.`,
},
3000,
);
},
);
}
deleteInvite(inviteId: string, notes: string) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Delete Invite?",
text: `Are you sure you want to erase the invite for "${notes}"? (There is no undo.)`,
onYes: async () => {
const headers = await getHeaders(this.activeDid);
try {
const result = await axios.delete(
this.apiServer + "/api/userUtil/invite/" + inviteId,
{ headers },
);
if (result.status !== 204) {
throw result.data;
}
this.invites = this.invites.filter(
(invite) => invite.inviteIdentifier !== inviteId,
);
this.$notify(
{
group: "alert",
type: "success",
title: "Deleted",
text: "Invite deleted.",
},
3000,
);
} catch (e) {
this.lookForErrorAndNotify(
e,
"Error Deleting Invite",
"Got an error deleting your invite.",
);
}
},
},
-1,
);
}
}
</script>

View File

@@ -1,335 +0,0 @@
<template>
<QuickNav selected="Home"></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 -->
<fa
icon="chevron-left"
@click="$router.back()"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
/>
New Activity For You
</h1>
</div>
<!-- Display a single row with the name of "New Offers To You" with a count. -->
<div class="flex justify-between" data-testId="showOffersToUser">
<div>
<span class="text-lg font-medium"
>{{ newOffersToUser.length
}}{{ newOffersToUserHitLimit ? "+" : "" }}</span
>
<span class="text-lg font-medium ml-4"
>New Offer{{ newOffersToUser.length === 1 ? "" : "s" }} To You</span
>
<fa
v-if="newOffersToUser.length > 0"
:icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
class="cursor-pointer ml-4 mr-4 text-lg"
@click="expandOffersToUserAndMarkRead()"
/>
</div>
<router-link to="/recent-offers-to-user" class="text-blue-500">
See&nbsp;all
</router-link>
</div>
<div v-if="showOffersDetails" class="ml-4 mt-4">
<ul class="list-disc ml-4">
<li
v-for="offer in newOffersToUser"
:key="offer.jwtId"
class="mt-4 relative group"
>
<span>{{
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
}}</span>
offered
<span v-if="offer.objectDescription">{{
offer.objectDescription
}}</span
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
<span v-if="offer.amount">{{
displayAmount(offer.unit, offer.amount)
}}</span>
<router-link
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
</router-link>
<!-- New line that appears on hover -->
<div
@click="markOffersAsReadStartingWith(offer.jwtId)"
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
>
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as new offers
</div>
</li>
</ul>
</div>
<!-- Display a single row with the name of "New Offers To Your Projects" with a count. -->
<div
class="mt-4 flex justify-between"
data-testId="showOffersToUserProjects"
>
<div>
<span class="text-lg font-medium"
>{{ newOffersToUserProjects.length
}}{{ newOffersToUserProjectsHitLimit ? "+" : "" }}</span
>
<span class="text-lg font-medium ml-4"
>New Offer{{ newOffersToUserProjects.length === 1 ? "" : "s" }} To
Your Projects</span
>
<fa
v-if="newOffersToUserProjects.length > 0"
:icon="
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
"
class="cursor-pointer ml-4 mr-4 text-lg"
@click="expandOffersToUserProjectsAndMarkRead()"
/>
</div>
<router-link to="/recent-offers-to-user-projects" class="text-blue-500">
See&nbsp;all
</router-link>
</div>
<div v-if="showOffersToUserProjectsDetails" class="ml-4 mt-4">
<ul class="list-disc ml-4">
<li
v-for="offer in newOffersToUserProjects"
:key="offer.jwtId"
class="mt-4 relative group"
>
<span>{{
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
}}</span>
offered
<span v-if="offer.objectDescription">{{
offer.objectDescription
}}</span
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
<span v-if="offer.amount">{{
displayAmount(offer.unit, offer.amount)
}}</span>
to
<span>{{ offer.planName }}</span>
<router-link
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
</router-link>
<!-- New line that appears on hover -->
<div
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)"
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
>
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as new offers
</div>
</li>
</ul>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { NotificationIface } from "@/constants/app";
import {
accountsDB,
db,
retrieveSettingsForActiveAccount,
updateAccountSettings,
} from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import {
didInfo,
displayAmount,
getNewOffersToUser,
getNewOffersToUserProjects,
OfferSummaryRecord,
OfferToPlanSummaryRecord,
} from "@/libs/endorserServer";
@Component({
components: { GiftedDialog, QuickNav, EntityIcon },
})
export default class NewActivityView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: string[] = [];
apiServer = "";
lastAckedOfferToUserJwtId = "";
lastAckedOfferToUserProjectsJwtId = "";
newOffersToUser: Array<OfferSummaryRecord> = [];
newOffersToUserHitLimit = false;
newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = [];
newOffersToUserProjectsHitLimit = false;
showOffersDetails = false;
showOffersToUserProjectsDetails = false;
didInfo = didInfo;
displayAmount = displayAmount;
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
if (allAccounts.length > 0) {
this.allMyDids = allAccounts.map((acc) => acc.did);
}
const offersToUserData = await getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserJwtId,
);
this.newOffersToUser = offersToUserData.data;
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
const offersToUserProjectsData = await getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
);
this.newOffersToUserProjects = offersToUserProjectsData.data;
this.newOffersToUserProjectsHitLimit = offersToUserProjectsData.hitLimit;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings & contacts:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your activity.",
},
5000,
);
}
}
async expandOffersToUserAndMarkRead() {
this.showOffersDetails = !this.showOffersDetails;
if (this.showOffersDetails) {
await updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
});
// note that we don't update this.lastAckedOfferToUserJwtId in case they
// later choose the last one to keep the offers as new
this.$notify(
{
group: "alert",
type: "info",
title: "Marked as Read",
text: "The offers are marked as viewed. Click in the list to keep them as new.",
},
5000,
);
}
}
async markOffersAsReadStartingWith(jwtId: string) {
const index = this.newOffersToUser.findIndex(
(offer) => offer.jwtId === jwtId,
);
if (index !== -1 && index < this.newOffersToUser.length - 1) {
// Set to the next offer's jwtId
await updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
});
} else {
// it's the last entry (or not found), so just keep it the same
await updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
});
}
this.$notify(
{
group: "alert",
type: "info",
title: "Marked as Unread",
text: "All offers above that one are marked as unread.",
},
3000,
);
}
async expandOffersToUserProjectsAndMarkRead() {
this.showOffersToUserProjectsDetails =
!this.showOffersToUserProjectsDetails;
if (this.showOffersToUserProjectsDetails) {
await updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId,
});
// note that we don't update this.lastAckedOfferToUserProjectsJwtId in case
// they later choose the last one to keep the offers as new
this.$notify(
{
group: "alert",
type: "info",
title: "Marked as Read",
text: "The offers are marked as viewed. Click in the list to keep them as new.",
},
5000,
);
}
}
async markOffersToUserProjectsAsReadStartingWith(jwtId: string) {
const index = this.newOffersToUserProjects.findIndex(
(offer) => offer.jwtId === jwtId,
);
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
// Set to the next offer's jwtId
await updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[index + 1].jwtId,
});
} else {
// it's the last entry (or not found), so just keep it the same
await updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.lastAckedOfferToUserProjectsJwtId,
});
}
this.$notify(
{
group: "alert",
type: "info",
title: "Marked as Unread",
text: "All offers above that one are marked as unread.",
},
3000,
);
}
}
</script>

View File

@@ -47,8 +47,8 @@
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component({
components: {},
@@ -58,10 +58,11 @@ export default class NewEditAccountView extends Vue {
// 'created' hook runs when the Vue instance is first created
async created() {
const settings = await retrieveSettingsForActiveAccount();
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.givenName =
(settings.firstName || "") +
(settings.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
}
async onClickSaveChanges() {

View File

@@ -105,11 +105,13 @@
<span class="col-span-1 w-full flex justify-center">{{ zoneName }}</span>
</div>
<div
class="flex items-center mb-4"
@click="includeLocation = !includeLocation"
>
<input type="checkbox" class="mr-2" v-model="includeLocation" />
<div class="flex items-center mb-4">
<input
type="checkbox"
class="mr-2"
v-model="includeLocation"
@click="includeLocation = !includeLocation"
/>
<label for="includeLocation">Include Location</label>
</div>
<div v-if="includeLocation" class="mb-4 aspect-video">
@@ -143,22 +145,6 @@
</l-map>
</div>
<div
v-if="showGeneralAdvanced && includeLocation"
class="items-center mb-4"
>
<div class="flex" @click="sendToTrustroots = !sendToTrustroots">
<input type="checkbox" class="mr-2" v-model="sendToTrustroots" />
<label>Send to Trustroots</label>
</div>
<!--
<div class="flex" @click="sendToTripHopping = !sendToTripHopping">
<input type="checkbox" class="mr-2" v-model="sendToTripHopping" />
<label>Send to TripHopping</label>
</div>
-->
</div>
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
@@ -192,35 +178,28 @@
import "leaflet/dist/leaflet.css";
import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon";
import { hexToBytes } from "@noble/hashes/utils";
import type { EventTemplate, VerifiedEvent } from "nostr-tools/lib/types/core";
import { accountFromSeedWords } from "nostr-tools/nip06";
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Router } from "vue-router";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import {
DEFAULT_IMAGE_API_SERVER,
DEFAULT_PARTNER_API_SERVER,
NotificationIface,
} from "@/constants/app";
import { accountsDB, retrieveSettingsForActiveAccount } from "@/db/index";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
createEndorserJwtVcFromClaim,
getHeaders,
PlanVerifiableCredential,
} from "@/libs/endorserServer";
import { getAccount } from "@/libs/util";
import { useAppStore } from "@/store/app";
@Component({
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
})
export default class NewEditProjectView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
errNote(message: string) {
errNote(message) {
this.$notify(
{ group: "alert", type: "danger", title: "Error", text: message },
5000,
@@ -245,11 +224,8 @@ export default class NewEditProjectView extends Vue {
latitude = 0;
longitude = 0;
numAccounts = 0;
projectId = "";
projectId = localStorage.getItem("projectId") || "";
projectIssuerDid = "";
sendToTrustroots = false;
sendToTripHopping = false;
showGeneralAdvanced = false;
startDateInput?: string;
startTimeInput?: string;
zoneName = DateTime.local().zoneName;
@@ -259,13 +235,10 @@ export default class NewEditProjectView extends Vue {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.projectId =
(this.$route as RouteLocationNormalizedLoaded).query["projectId"] || "";
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
if (this.projectId) {
if (this.numAccounts === 0) {
@@ -384,7 +357,7 @@ export default class NewEditProjectView extends Vue {
}
}
private async saveProject() {
private async saveProject(issuerDid: string) {
// Make a claim
const vcClaim: PlanVerifiableCredential = this.fullClaim;
if (this.projectId) {
@@ -435,44 +408,24 @@ export default class NewEditProjectView extends Vue {
} else {
delete vcClaim.startTime;
}
const vcJwt = await createEndorserJwtVcFromClaim(this.activeDid, vcClaim);
const vcJwt = await createEndorserJwtVcFromClaim(issuerDid, vcClaim);
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
const headers = await getHeaders(this.activeDid);
const headers = await getHeaders(issuerDid);
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) {
this.errorMessage = "";
const projectPath = encodeURIComponent(resp.data.success.handleId);
let signedPayload: VerifiedEvent | undefined; // sign something to prove ownership of pubkey
if (this.sendToTrustroots) {
signedPayload = await this.signPayload();
this.sendToNostrPartner(
"NOSTR-EVENT-TRUSTROOTS",
"Trustroots",
resp.data.success.claimId,
signedPayload,
);
}
if (this.sendToTripHopping) {
if (!signedPayload) {
signedPayload = await this.signPayload();
}
this.sendToNostrPartner(
"NOSTR-EVENT-TRIPHOPPING",
"TripHopping",
resp.data.success.claimId,
signedPayload,
);
}
(this.$router as Router).push({ path: "/project/" + projectPath });
useAppStore()
.setProjectId(resp.data.success.handleId)
.then(() => {
(this.$router as Router).push({ name: "project" });
});
} else {
console.error(
"Got unexpected 'data' inside response from server",
@@ -536,120 +489,6 @@ export default class NewEditProjectView extends Vue {
}
}
private async signPayload(): Promise<VerifiedEvent> {
const account = await getAccount(this.activeDid);
// get the last number of the derivationPath
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
// remove any trailing '
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
const accountNum = Number(finalDerNumNoApostrophe || 0);
const pubPri = accountFromSeedWords(
account?.mnemonic as string,
"",
accountNum,
);
const privateBytes = hexToBytes(pubPri?.privateKey);
// No real content is necessary, we just want something signed,
// so we might as well use nostr libs for nostr functions.
// Besides: someday we may create real content that we can relay.
const event: EventTemplate = {
kind: 30402,
tags: [[]],
content: "",
created_at: 0,
};
// Why does IntelliJ not see matching types?
const signedEvent = finalizeEvent(event, privateBytes);
return signedEvent;
}
private async sendToNostrPartner(
linkCode: string,
serviceName: string,
jwtId: string,
signedPayload: VerifiedEvent,
) {
// first, get the public key for nostr
const account = await getAccount(this.activeDid);
// get the last number of the derivationPath
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
// remove any trailing '
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
const accountNum = Number(finalDerNumNoApostrophe || 0);
const pubPri = accountFromSeedWords(
account?.mnemonic as string,
"",
accountNum,
);
const nostrPubKey = pubPri?.publicKey;
let partnerServer = DEFAULT_PARTNER_API_SERVER;
const settings = await retrieveSettingsForActiveAccount();
if (settings.partnerApiServer) {
partnerServer = settings.partnerApiServer;
}
const trustrootsUrl = partnerServer + "/api/partner/link";
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
const content = this.fullClaim.name + " - see " + timeSafariUrl;
// Why does IntelliJ not see matching types?
const payload = serializeEvent(signedPayload);
const trustrootsParams = {
jwtId: jwtId,
linkCode: linkCode,
inputJson: JSON.stringify(content),
pubKeyHex: nostrPubKey,
pubKeyImage: payload,
pubKeySigHex: signedPayload.sig,
};
const fullTrustrootsUrl = trustrootsUrl;
const headers = await getHeaders(this.activeDid);
try {
const linkResp = await this.axios.post(
fullTrustrootsUrl,
trustrootsParams,
{ headers },
);
if (linkResp.status === 201) {
this.$notify(
{
group: "alert",
type: "success",
title: `Sent to ${serviceName}`,
text: `The project info was sent to ${serviceName}.`,
},
5000,
);
} else {
// axios never gets here because it throws an error, but just in case
this.$notify(
{
group: "alert",
type: "danger",
title: `Failed Sending to ${serviceName}`,
text: JSON.stringify(linkResp.data),
},
5000,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error(`Error sending to ${serviceName}`, error);
let errorMessage = `There was an error sending to ${serviceName}.`;
if (error.response?.data?.error?.message) {
errorMessage = error.response.data.error.message;
}
this.$notify(
{
group: "alert",
type: "danger",
title: `Error Sending to ${serviceName}`,
text: errorMessage,
},
5000,
);
}
}
public async onSaveProjectClick() {
this.isHiddenSave = true;
this.isHiddenSpinner = false;
@@ -657,7 +496,7 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) {
console.error("Error: there is no account.");
} else {
this.saveProject();
this.saveProject(this.activeDid);
}
}

View File

@@ -35,7 +35,7 @@
<textarea
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What is offered"
v-model="descriptionOfItem"
v-model="itemDescription"
data-testId="itemDescription"
/>
<div class="flex flex-row justify-center">
@@ -74,7 +74,7 @@
<textarea
class="w-full border border-slate-400 px-3 py-2 rounded-r"
placeholder="Prerequisites, other people to include, etc."
v-model="descriptionOfCondition"
v-model="conditionDescription"
/>
</div>
@@ -135,7 +135,7 @@
</label>
</div>
<div v-if="showGeneralAdvanced" class="mt-4 flex">
<div class="mt-4 flex">
<router-link
:to="{
name: 'claim-add-raw',
@@ -181,7 +181,8 @@ import { Router } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import {
createAndSubmitOffer,
didInfo,
@@ -207,21 +208,20 @@ export default class OfferDetailsView extends Vue {
apiServer = "";
amountInput = "0";
descriptionOfCondition = "";
descriptionOfItem = "";
conditionDescription = "";
itemDescription = "";
destinationPathAfter = "";
hideBackButton = false;
message = "";
offeredToProject = false;
offeredToRecipient = false;
offererDid: string | undefined;
hideBackButton = false;
message = "";
offerId = "";
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
projectId = "";
projectName = "a project";
recipientDid = "";
recipientName = "";
showGeneralAdvanced = false;
unitCode = "HUR";
validThroughDateInput = "";
@@ -256,12 +256,12 @@ export default class OfferDetailsView extends Vue {
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
this.unitCode) as string;
this.descriptionOfCondition =
this.prevCredToEdit?.claim?.description || this.descriptionOfCondition;
this.descriptionOfItem =
this.conditionDescription =
this.prevCredToEdit?.claim?.description || this.conditionDescription;
this.itemDescription =
(this.$route as Router).query["description"] ||
this.prevCredToEdit?.claim?.itemOffered?.description ||
this.descriptionOfItem;
this.itemDescription;
this.destinationPathAfter = (this.$route as Router).query[
"destinationPathAfter"
];
@@ -296,10 +296,10 @@ export default class OfferDetailsView extends Vue {
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer ?? "";
this.activeDid = settings.activeDid ?? "";
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
let allContacts: Contact[] = [];
let allMyDids: string[] = [];
@@ -402,7 +402,7 @@ export default class OfferDetailsView extends Vue {
);
return;
}
if (!this.descriptionOfItem && !parseFloat(this.amountInput)) {
if (!this.itemDescription && !parseFloat(this.amountInput)) {
this.$notify(
{
group: "alert",
@@ -502,10 +502,10 @@ export default class OfferDetailsView extends Vue {
this.apiServer,
this.prevCredToEdit,
this.activeDid,
this.descriptionOfItem,
this.itemDescription,
parseFloat(this.amountInput),
this.unitCode,
this.descriptionOfCondition,
this.conditionDescription,
this.validThroughDateInput,
recipientDid,
projectId,
@@ -515,10 +515,10 @@ export default class OfferDetailsView extends Vue {
this.axios,
this.apiServer,
this.activeDid,
this.descriptionOfItem,
this.itemDescription,
parseFloat(this.amountInput),
this.unitCode,
this.descriptionOfCondition,
this.conditionDescription,
this.validThroughDateInput,
recipientDid,
projectId,
@@ -582,10 +582,10 @@ export default class OfferDetailsView extends Vue {
this.prevCredToEdit?.claim as OfferVerifiableCredential,
this.activeDid,
recipientDid,
this.descriptionOfItem,
this.itemDescription,
parseFloat(this.amountInput),
this.unitCode,
this.descriptionOfCondition,
this.conditionDescription,
projectId,
this.validThroughDateInput,
this.prevCredToEdit?.id as string,

View File

@@ -220,10 +220,7 @@
</li>
</ul>
<!--
Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list
(we want to limit the grid count above to 8 or 12 accounts to keep it compact)
-->
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
<a
v-if="allContacts.length >= 7"
@click="onClickAllContactsGifting()"
@@ -385,7 +382,7 @@
<span>
{{
serverUtil.didInfo(
give.recipientDid,
give.agentDid,
activeDid,
allMyDids,
allContacts,
@@ -434,14 +431,16 @@ import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import * as libsUtil from "@/libs/util";
import {
BLANK_GENERIC_SERVER_RECORD,
GenericCredWrapper,
getHeaders,
GiverReceiverInputInfo,
GiveSummaryRecord,
GiveVerifiableCredential,
OfferSummaryRecord,
@@ -485,7 +484,7 @@ export default class ProjectViewView extends Vue {
name = "";
offersToThis: Array<OfferSummaryRecord> = [];
offersHitLimit = false;
projectId = ""; // handle ID
projectId = localStorage.getItem("projectId") || ""; // handle ID
showDidCopy = false;
startTime = "";
truncatedDesc = "";
@@ -496,11 +495,12 @@ export default class ProjectViewView extends Vue {
serverUtil = serverUtil;
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
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;
this.isRegistered = !!settings?.isRegistered;
await accountsDB.open();
const accounts = accountsDB.accounts;
@@ -515,9 +515,9 @@ export default class ProjectViewView extends Vue {
}
onEditClick() {
localStorage.setItem("projectId", this.projectId as string);
const route = {
name: "new-edit-project",
query: { projectId: this.projectId },
};
(this.$router as Router).push(route);
}
@@ -566,22 +566,35 @@ export default class ProjectViewView extends Vue {
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem getting that project.",
text: "There was a problem getting that project. See logs for more info.",
},
5000,
);
}
} catch (error: unknown) {
console.error("Error retrieving project:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving that project.",
},
5000,
);
const serverError = error as AxiosError;
if (serverError.response?.status === 404) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "That project does not exist.",
},
5000,
);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving that project. See logs for more info.",
},
5000,
);
}
}
this.loadGives();
@@ -826,6 +839,7 @@ export default class ProjectViewView extends Vue {
* @param id of the project
**/
async onClickLoadProject(projectId: string) {
localStorage.setItem("projectId", projectId);
const route = {
path: "/project/" + encodeURIComponent(projectId),
};
@@ -847,7 +861,7 @@ export default class ProjectViewView extends Vue {
);
}
openGiftDialog(contact?: libsUtil.GiverReceiverInputInfo) {
openGiftDialog(contact?: GiverReceiverInputInfo) {
(this.$refs.customGiveDialog as GiftedDialog).open(
contact,
undefined,
@@ -861,11 +875,9 @@ export default class ProjectViewView extends Vue {
}
onClickAllContactsGifting() {
localStorage.setItem("projectId", this.projectId);
const route = {
name: "contact-gift",
query: {
projectId: this.projectId,
},
};
(this.$router as Router).push(route);
}
@@ -893,7 +905,7 @@ export default class ProjectViewView extends Vue {
claim: offer.fullClaim,
issuer: offer.offeredByDid,
};
const giver: libsUtil.GiverReceiverInputInfo = {
const giver: GiverReceiverInputInfo = {
did: libsUtil.offerGiverDid(offerRecord),
};
(this.$refs.customGiveDialog as GiftedDialog).open(
@@ -998,7 +1010,7 @@ export default class ProjectViewView extends Vue {
console.error("Got error submitting the confirmation:", result);
const message =
(result.error?.error as string) ||
"There was a problem submitting the confirmation.";
"There was a problem submitting the confirmation. See logs for more info.";
this.$notify(
{
group: "alert",

View File

@@ -6,8 +6,6 @@
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light">Your Ideas</h1>
<OnboardingDialog ref="onboardingDialog" />
<!-- Result Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300 mt-8">
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
@@ -63,7 +61,7 @@
<!-- New Project -->
<button
v-if="isRegistered && showProjects"
class="fixed right-6 top-24 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
class="fixed right-6 top-24 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full"
@click="onClickNewProject()"
>
<fa icon="plus" class="fa-fw"></fa>
@@ -152,10 +150,7 @@
<span
v-if="offer.amountGivenConfirmed >= offer.amountGiven"
>
<!--
There's no need for a green icon:
it's unnecessary if there's already a green, and confusing if there's a yellow.
-->
<!-- no need for green icon; unnecessary if there's already a green, confusing if there's a yellow -->
all
</span>
<span v-else>
@@ -209,19 +204,10 @@
Hit the big
<fa
icon="plus"
class="bg-green-600 text-white px-1.5 py-1 rounded-full"
class="bg-blue-600 text-white px-1 py-1 rounded-full"
/>
button. You'll never know until you try.
</div>
<div v-else>
<button
@click="showNameThenIdDialog()"
class="text-md font-bold 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-2 py-3 rounded-md"
>
Get someone to onboard you.
</button>
<UserNameDialog ref="userNameDialog" />
</div>
</div>
<ul id="listProjects" class="border-t border-slate-300">
<li
@@ -261,16 +247,13 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as libsUtil from "@/libs/util";
import EntityIcon from "@/components/EntityIcon.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import OnboardingDialog from "@/components/OnboardingDialog.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue";
import UserNameDialog from "@/components/UserNameDialog.vue";
import { Contact } from "@/db/tables/contacts";
import {
didInfo,
getHeaders,
@@ -278,18 +261,11 @@ import {
OfferSummaryRecord,
PlanData,
} from "@/libs/endorserServer";
import { OnboardPage } from "@/libs/util";
import EntityIcon from "@/components/EntityIcon.vue";
import { Contact } from "@/db/tables/contacts";
@Component({
components: {
EntityIcon,
InfiniteScroll,
QuickNav,
OnboardingDialog,
ProjectIcon,
TopMessage,
UserNameDialog,
},
components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
})
export default class ProjectsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -304,25 +280,24 @@ export default class ProjectsView extends Vue {
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
givenName = "";
projects: PlanData[] = [];
isLoading = false;
isRegistered = false;
offers: OfferSummaryRecord[] = [];
projectNameFromHandleId: Record<string, string> = {}; // mapping from handleId to description
projects: PlanData[] = [];
showOffers = false;
showProjects = true;
showOffers = true;
showProjects = false;
libsUtil = libsUtil;
didInfo = didInfo;
async mounted() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
this.givenName = settings.firstName || "";
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || "";
this.isRegistered = !!settings?.isRegistered;
this.allContacts = await db.contacts.toArray();
@@ -330,17 +305,11 @@ export default class ProjectsView extends Vue {
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Create,
);
}
if (allAccounts.length === 0) {
console.error("No accounts found.");
this.errNote("You need an identifier to load your projects.");
} else {
await this.loadProjects();
await this.loadOffers();
}
} catch (err) {
console.error("Error initializing:", err);
@@ -414,6 +383,7 @@ export default class ProjectsView extends Vue {
* @param id of the project
**/
onClickLoadProject(id: string) {
localStorage.setItem("projectId", id);
const route = {
path: "/project/" + encodeURIComponent(id),
};
@@ -424,6 +394,7 @@ export default class ProjectsView extends Vue {
* Handling clicking on the new project button
**/
onClickNewProject(): void {
localStorage.removeItem("projectId");
const route = {
name: "new-edit-project",
};
@@ -459,8 +430,18 @@ export default class ProjectsView extends Vue {
this.activeDid,
);
const projectName = project?.name as string;
console.log(
"now have name for",
offer.fulfillsPlanHandleId,
projectName,
);
this.projectNameFromHandleId[offer.fulfillsPlanHandleId] =
projectName;
console.log(
"now have a real name for",
offer.fulfillsPlanHandleId,
this.projectNameFromHandleId[offer.fulfillsPlanHandleId],
);
}
this.offers = this.offers.concat([offer]);
}
@@ -518,37 +499,6 @@ export default class ProjectsView extends Vue {
await this.offerDataLoader(url);
}
showNameThenIdDialog() {
if (!this.givenName) {
(this.$refs.userNameDialog as UserNameDialog).open(() => {
this.promptForShareMethod();
});
} else {
this.promptForShareMethod();
}
}
promptForShareMethod() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Are you nearby with cameras?",
text: "If so, we'll use those with QR codes to share.",
onCancel: async () => {},
onNo: async () => {
(this.$router as Router).push({ name: "share-my-contact-info" });
},
onYes: async () => {
(this.$router as Router).push({ name: "contact-qr" });
},
noText: "we will share another way",
yesText: "we are nearby with cameras",
},
-1,
);
}
public computedOfferTabClassNames() {
return {
"inline-block": true,

View File

@@ -67,13 +67,12 @@
<script lang="ts">
import axios from "axios";
import { DateTime } from "luxon";
import { Router } from "vue-router";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { retrieveSettingsForActiveAccount } from "@/db/index";
import { db } from "@/db/index";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
bvcMeetingJoinClaim,
@@ -81,6 +80,7 @@ import {
createAndSubmitGive,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component({
components: {
@@ -117,9 +117,10 @@ export default class QuickActionBvcBeginView extends Vue {
}
async record() {
const settings = await retrieveSettingsForActiveAccount();
const activeDid = settings.activeDid || "";
const apiServer = settings.apiServer || "";
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer || "";
try {
const hoursNum = libsUtil.numberOrZero(this.hoursStr);
@@ -201,7 +202,6 @@ export default class QuickActionBvcBeginView extends Vue {
},
3000,
);
(this.$router as Router).push({ path: "/quick-action-bvc" });
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -97,7 +97,7 @@
<h2 class="text-2xl m-2">Anything else?</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="someoneGave" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">The group provided</span>
<span class="pb-2 pl-2 pr-2">Someone else gave</span>
<span v-if="someoneGave">
<input
type="text"
@@ -106,8 +106,7 @@
class="border border-slate-400 h-6 px-2"
/>
<br />
(Everyone likes personalized messages! 😁 ... and for a pic:
<input type="checkbox" v-model="supplyGiftDetails" />)
(Everyone likes personalized messages! 😁)
</span>
<!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6">...</span>
@@ -145,8 +144,9 @@ import { Router } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription,
@@ -180,16 +180,17 @@ export default class QuickActionBvcBeginView extends Vue {
description = "breakfast";
loadingConfirms = true;
someoneGave = false;
supplyGiftDetails = false;
async created() {
this.loadingConfirms = true;
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
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();
}
async mounted() {
this.loadingConfirms = true;
let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
if (currentOrPreviousSat.weekday < 6) {
// it's not Saturday or Sunday,
@@ -264,9 +265,7 @@ export default class QuickActionBvcBeginView extends Vue {
async record() {
try {
if (this.claimsToConfirmSelected.length > 0) {
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
}
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
// in parallel, make a confirmation for each selected claim and send them all to the server
const confirmResults = await Promise.allSettled(
@@ -308,7 +307,7 @@ export default class QuickActionBvcBeginView extends Vue {
// now send the give for the description
let giveSucceeded = false;
if (this.someoneGave && !this.supplyGiftDetails) {
if (this.someoneGave) {
const giveResult = await createAndSubmitGive(
axios,
this.apiServer,
@@ -318,10 +317,6 @@ export default class QuickActionBvcBeginView extends Vue {
this.description,
undefined,
undefined,
undefined,
undefined,
false,
undefined,
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
giveSucceeded = giveResult.type === "success";
@@ -340,60 +335,29 @@ export default class QuickActionBvcBeginView extends Vue {
);
}
}
if (this.someoneGave && this.supplyGiftDetails) {
// we'll give a success message for the confirmations and go to the gifted details page
if (confirmsSucceeded.length > 0) {
const actions =
confirmsSucceeded.length === 1
? `Your confirmation has been recorded.`
: `Your confirmations have been recorded.`;
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: actions,
},
3000,
);
}
(this.$router as Router).push({
name: "gifted-details",
query: {
description: this.description,
destinationPathAfter: "/",
providerProjectId: BVC_MEETUPS_PROJECT_CLAIM_ID,
recipientDid: this.activeDid,
if (confirmsSucceeded.length > 0 || giveSucceeded) {
const confirms =
confirmsSucceeded.length === 1 ? "confirmation" : "confirmations";
const actions =
confirmsSucceeded.length > 0 && giveSucceeded
? `Your ${confirms} and that give have been recorded.`
: giveSucceeded
? "That give has been recorded."
: "Your " +
confirms +
" " +
(confirmsSucceeded.length === 1 ? "has" : "have") +
" been recorded.";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: actions,
},
});
} else {
// just go ahead and print a message for all the activity
if (confirmsSucceeded.length > 0 || giveSucceeded) {
const confirms =
confirmsSucceeded.length === 1 ? "confirmation" : "confirmations";
const actions =
confirmsSucceeded.length > 0 && giveSucceeded
? `Your ${confirms} and that give have been recorded.`
: giveSucceeded
? "That give has been recorded."
: "Your " +
confirms +
" " +
(confirmsSucceeded.length === 1 ? "has" : "have") +
" been recorded.";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: actions,
},
3000,
);
(this.$router as Router).push({ path: "/" });
} else {
// errors should have already shown
}
3000,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,169 +0,0 @@
<template>
<QuickNav selected="Home"></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 -->
<fa
icon="chevron-left"
@click="$router.back()"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
/>
Offers to Your Projects
</h1>
</div>
<div v-if="newOffersToUserProjects.length === 0">
<p>Nobody has given any offers to your projects.</p>
<p class="mt-2">
Maybe there are already some projects you can help on the
<router-link to="/discover" class="text-blue-500">
Discover page <fa icon="search" />
</router-link>
</p>
<p class="mt-2">
You can announce more of your own on
<router-link to="/contacts" class="text-blue-500">
Your Ideas page <fa icon="hand" />
</router-link>
</p>
</div>
<InfiniteScroll @reached-bottom="loadMoreOffersToUserProjects">
<ul
data-testId="listRecentOffersToUserProjects"
class="border-t border-slate-300"
>
<li
v-for="offer in newOffersToUserProjects"
:key="offer.jwtId"
class="mt-4 relative group"
>
<div
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
v-if="offer.jwtId == lastAckedOfferToUserProjectsJwtId"
>
You've already seen all the following
</div>
<span>{{
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
}}</span>
offered
<span v-if="offer.objectDescription">{{
offer.objectDescription
}}</span
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
<span v-if="offer.amount">{{
displayAmount(offer.unit, offer.amount)
}}</span>
to
<span>{{ offer.planName }}</span>
<router-link
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
</router-link>
</li>
</ul>
</InfiniteScroll>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import {
didInfo,
displayAmount,
getNewOffersToUserProjects,
OfferToPlanSummaryRecord,
} from "@/libs/endorserServer";
@Component({
components: { EntityIcon, GiftedDialog, InfiniteScroll, QuickNav },
})
export default class RecentOffersToUserView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: string[] = [];
apiServer = "";
lastAckedOfferToUserProjectsJwtId = "";
newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = [];
newOffersToUserProjectsAtEnd = false;
showOffersDetails = false;
showOffersToUserProjectsDetails = false;
didInfo = didInfo;
displayAmount = displayAmount;
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
if (allAccounts.length > 0) {
this.allMyDids = allAccounts.map((acc) => acc.did);
}
const offersToUserProjectsData = await getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
undefined,
undefined,
);
this.newOffersToUserProjects = offersToUserProjectsData.data;
this.newOffersToUserProjectsAtEnd = !offersToUserProjectsData.hitLimit;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings & contacts:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your activity.",
},
5000,
);
}
}
async loadMoreOffersToUserProjects() {
if (this.newOffersToUserProjectsAtEnd) {
return;
}
const offersToUserProjectsData = await getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
undefined,
this.newOffersToUserProjects[this.newOffersToUserProjects.length - 1]
.jwtId,
);
this.newOffersToUserProjects.push(...offersToUserProjectsData.data);
this.newOffersToUserProjectsAtEnd = !offersToUserProjectsData.hitLimit;
}
}
</script>

View File

@@ -1,160 +0,0 @@
<template>
<QuickNav selected="Home"></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 -->
<fa
icon="chevron-left"
@click="$router.back()"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
/>
Offers to You
</h1>
</div>
<div v-if="newOffersToUser.length === 0">
<p>Nobody has given you an offer.</p>
<p class="mt-2">
You can start the cycle on the
<router-link to="/contacts" class="text-blue-500">
Contacts page <fa icon="users" />
</router-link>
with an "Offer" directly to someone. Hopefully you'll find a common
interest!
</p>
</div>
<InfiniteScroll @reached-bottom="loadMoreOffersToUser">
<ul
data-testId="listRecentOffersToUser"
class="border-t border-slate-300"
>
<li
v-for="offer in newOffersToUser"
:key="offer.jwtId"
class="mt-4 relative group"
>
<div
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
v-if="offer.jwtId == lastAckedOfferToUserJwtId"
>
You've already seen all the following
</div>
<span>{{
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
}}</span>
offered
<span v-if="offer.objectDescription">{{
offer.objectDescription
}}</span
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
<span v-if="offer.amount">{{
displayAmount(offer.unit, offer.amount)
}}</span>
<router-link
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
</router-link>
</li>
</ul>
</InfiniteScroll>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import {
didInfo,
displayAmount,
getNewOffersToUser,
OfferSummaryRecord,
} from "@/libs/endorserServer";
@Component({
components: { EntityIcon, GiftedDialog, InfiniteScroll, QuickNav },
})
export default class RecentOffersToUserView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: string[] = [];
apiServer = "";
lastAckedOfferToUserJwtId = "";
newOffersToUser: Array<OfferSummaryRecord> = [];
newOffersToUserAtEnd = false;
showOffersDetails = false;
showOffersToUserProjectsDetails = false;
didInfo = didInfo;
displayAmount = displayAmount;
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
if (allAccounts.length > 0) {
this.allMyDids = allAccounts.map((acc) => acc.did);
}
const offersToUserData = await getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
undefined,
undefined,
);
this.newOffersToUser = offersToUserData.data;
this.newOffersToUserAtEnd = !offersToUserData.hitLimit;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings & contacts:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your activity.",
},
5000,
);
}
}
async loadMoreOffersToUser() {
if (this.newOffersToUserAtEnd) {
return;
}
const offersToUserData = await getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
undefined,
this.newOffersToUser[this.newOffersToUser.length - 1].jwtId,
);
this.newOffersToUser.push(...offersToUserData.data);
this.newOffersToUserAtEnd = !offersToUserData.hitLimit;
}
}
</script>

View File

@@ -26,7 +26,7 @@
your device to run searches but it is not stored on our servers.
</div>
<div class="text-center">
<div>
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
Click to Choose a Location for Nearby Search
</button>
@@ -35,7 +35,6 @@
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="storeSearchBox"
>
<fa icon="save" class="fa-fw" />
Store This Location for Nearby Search
</button>
<button
@@ -43,7 +42,6 @@
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="forgetSearchBox"
>
<fa icon="trash-can" class="fa-fw" />
Delete Stored Location
</button>
<button
@@ -51,15 +49,13 @@
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="resetLatLong"
>
<fa icon="rotate" class="fa-fw" />
Reset To Original
Reset Marker
</button>
<button
v-if="isNewMarkerSet"
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="isNewMarkerSet = false"
>
<fa icon="eraser" class="fa-fw" />
Erase Marker
</button>
<div v-if="isNewMarkerSet">
@@ -113,7 +109,7 @@ import { Router } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { db } from "@/db/index";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
const DEFAULT_LAT_LONG_DIFF = 0.01;
@@ -146,8 +142,9 @@ export default class DiscoverView extends Vue {
searchBox: { name: string; bbox: BoundingBox } | null = null;
async mounted() {
const settings = await retrieveSettingsForActiveAccount();
this.searchBox = settings.searchBoxes?.[0] || null;
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.searchBox = settings?.searchBoxes?.[0] || null;
this.resetLatLong();
}

View File

@@ -105,8 +105,9 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({ components: { QuickNav } })
export default class SeedBackupView extends Vue {
@@ -121,8 +122,9 @@ export default class SeedBackupView extends Vue {
// 'created' hook runs when the Vue instance is first created
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
const activeDid = settings.activeDid || "";
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();

View File

@@ -49,7 +49,8 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { generateEndorserJwtForAccount } from "@/libs/endorserServer";
@Component({
@@ -59,11 +60,12 @@ export default class ShareMyContactInfoView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
async onClickShare() {
const settings = await retrieveSettingsForActiveAccount();
const activeDid = settings.activeDid || "";
const givenName = settings.firstName || "";
const isRegistered = !!settings.isRegistered;
const profileImageUrl = settings.profileImageUrl || "";
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
const activeDid = settings?.activeDid || "";
const givenName = settings?.firstName || "";
const isRegistered = !!settings?.isRegistered;
const profileImageUrl = settings?.profileImageUrl || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
@@ -77,7 +79,6 @@ export default class ShareMyContactInfoView extends Vue {
isRegistered,
givenName,
profileImageUrl,
true,
);
useClipboard()
.copy(message)

View File

@@ -75,7 +75,7 @@ import {
IMAGE_TYPE_PROFILE,
NotificationIface,
} from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { base64ToBlob, SHARED_PHOTO_BASE64_KEY } from "@/libs/util";
@@ -94,8 +94,9 @@ export default class SharedPhotoView extends Vue {
// 'created' hook runs when the Vue instance is first created
async mounted() {
try {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid;
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid as string;
const temp = await db.temp.get(SHARED_PHOTO_BASE64_KEY);
const imageB64 = temp?.blobB64 as string;

View File

@@ -92,7 +92,8 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, PASSKEYS_ENABLED } from "@/constants/app";
import { accountsDB, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { registerSaveAndActivatePasskey } from "@/libs/util";
@Component({
@@ -105,8 +106,9 @@ export default class StartView extends Vue {
numAccounts = 0;
async mounted() {
const settings = await retrieveSettingsForActiveAccount();
this.givenName = settings.firstName || "";
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.givenName = settings?.firstName || "";
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();

View File

@@ -25,13 +25,12 @@
Here is a view of the activity you can see.
<ul class="list-disc outside ml-4">
<li>Each identity and claim has a unique position.</li>
<li>
Each will show at their time of appearance relative to all others.
</li>
<li>
Note that the ones on the left and right edges are randomized because
their data isn't all visible to you.
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
<li>Each will show at their time of appearance relative to all others.</li>
<li>Note that the ones on the left and right edges are randomized
because their data isn't all visible to you.
</li>
<!-- eslint-enable -->
</ul>
</div>
@@ -47,9 +46,7 @@
{{ worldProperties.animationDurationSeconds }} seconds
</div>
</div>
<button class="float-right text-blue-600" @click="captureGraphics()">
Screenshot
</button>
<button class="float-right text-blue-600" @click="captureGraphics()">Screenshot</button>
<div id="scene-container" class="h-screen"></div>
</section>
</template>

View File

@@ -157,7 +157,7 @@
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Image Sharing</h2>
Populates the "shared-photo" view as if they used "share_target".
<input type="file" data-testId="fileInput" @change="uploadFile" />
<input type="file" data-testid="fileInput" @change="uploadFile" />
<router-link
v-if="showFileNextStep()"
:to="{
@@ -165,7 +165,7 @@
query: { fileName },
}"
class="block w-full text-center text-md 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 mb-2 mt-2"
data-testId="fileUploadButton"
data-testid="fileUploadButton"
>
Go to Shared Page
</router-link>
@@ -247,7 +247,8 @@ import { Router } from "vue-router";
import QuickNav from "@/components/QuickNav.vue";
import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db, retrieveSettingsForActiveAccount } from "@/db/index";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as vcLib from "@/libs/crypto/vc";
import {
PeerSetup,
@@ -290,9 +291,10 @@ export default class Help extends Vue {
userName?: string;
async mounted() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.userName = settings.firstName;
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || "";
this.userName = settings?.firstName as string;
await accountsDB.open();
const account: { identity?: string } | undefined = await accountsDB.accounts

View File

@@ -5,7 +5,6 @@ importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
);
// similar method is in the src/db/index.ts file
function logConsoleAndDb(message, arg1, arg2) {
// in chrome://serviceworker-internals note that the arg1 and arg2 here will show as "[object Object]" in that page but will show as expandable objects in the console
console.log(`${new Date().toISOString()} ${message}`, arg1, arg2);
@@ -14,18 +13,10 @@ function logConsoleAndDb(message, arg1, arg2) {
if (appendDailyLog) {
let fullMessage = `${new Date().toISOString()} ${message}`;
if (arg1) {
if (typeof arg1 === "string") {
fullMessage += `\n${arg1}`;
} else {
fullMessage += `\n${JSON.stringify(arg1)}`;
}
fullMessage += `\n${JSON.stringify(arg1)}`;
}
if (arg2) {
if (typeof arg2 === "string") {
fullMessage += `\n${arg2}`;
} else {
fullMessage += `\n${JSON.stringify(arg2)}`;
}
fullMessage += `\n${JSON.stringify(arg2)}`;
}
// appendDailyLog is injected from safari-notifications.js at build time by the vue.config.js configureWebpack apply plugin
// eslint-disable-next-line no-undef
@@ -72,16 +63,16 @@ self.addEventListener("push", function (event) {
// See https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/c1ed026662e754348a5f91542680bd4f57e5b81e/app.py#L217
const DAILY_UPDATE_TITLE = "DAILY_CHECK";
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
// Make sure it is something different from the DAILY_UPDATE_TITLE.
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
// Make sure it is something other than the DAILY_UPDATE_TITLE.
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
let title;
let message = "Got some empty message.";
if (payload && payload.title == DIRECT_PUSH_TITLE) {
// skip any search logic and show the message directly
title = "Direct Message";
title = "Direct Notification";
message = payload.message || "No details were provided.";
} else {
// any other title will run through regular filtering logic
@@ -142,8 +133,7 @@ self.addEventListener("notificationclick", (event) => {
// This is invoked when the user chooses this as a share_target, mapped to share-target in the manifest.
self.addEventListener("fetch", (event) => {
// Skipping this because we get so many of them, at startup and other times, all with an event of: {isTrusted:true}
//logConsoleAndDb("Service worker got fetch event.", event);
logConsoleAndDb("Service worker got fetch event.", event);
// Bypass any regular requests not related to Web Share Target
// and also requests that are not exactly to the timesafari.app

View File

@@ -1,13 +1,12 @@
import { test, expect } from '@playwright/test';
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
import { generateEthrUser, importUser } from './testUtils';
test('Check activity feed - check that server is running', async ({ page }) => {
test('Check activity feed', async ({ page }) => {
// Load app homepage
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
// Check that initial 10 activities have been loaded
await expect(page.locator('ul#listLatestActivity li:nth-child(10)')).toBeVisible();
await page.locator('ul#listLatestActivity li:nth-child(10)');
// Scroll down a bit to trigger loading additional activities
await page.locator('ul#listLatestActivity li:nth-child(50)').scrollIntoViewIfNeeded();
@@ -18,7 +17,7 @@ test('Check discover results', async ({ page }) => {
await page.goto('./discover');
// Check that initial 10 projects have been loaded
await expect(page.locator('ul#listDiscoverResults li.border-b:nth-child(10)')).toBeVisible();
await page.locator('ul#listDiscoverResults li.border-b:nth-child(10)');
// Scroll down a bit to trigger loading additional projects
await page.locator('ul#listDiscoverResults li.border-b:nth-child(20)').scrollIntoViewIfNeeded();
@@ -38,17 +37,6 @@ test('Check no-ID messaging in account', async ({ page }) => {
await expect(page.locator('#sectionIdentityDetails code.truncate')).toBeEmpty();
});
test('Check ability to share contact', async ({ page }) => {
// Load Discover view
await page.goto('./discover');
// Check that initial 10 projects have been loaded
await expect(page.locator('ul#listDiscoverResults li.border-b:nth-child(10)')).toBeVisible();
// Scroll down a bit to trigger loading additional projects
await page.locator('ul#listDiscoverResults li.border-b:nth-child(20)').scrollIntoViewIfNeeded();
});
test('Check ID generation', async ({ page }) => {
// Load Account view
await page.goto('./account');
@@ -76,7 +64,6 @@ test('Check ID generation', async ({ page }) => {
test('Check setting name & sharing info', async ({ page }) => {
// Load homepage to trigger ID generation (?)
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
// Check 'someone must register you' notice
await expect(page.getByText('someone must register you.')).toBeVisible();
await page.getByRole('button', { name: /Show them/}).click();
@@ -96,7 +83,7 @@ test('Check setting name & sharing info', async ({ page }) => {
await expect(page.getByText('your contacts')).toBeVisible();
});
test('Confirm test API setting (may fail if you are running your own Time Safari)', async ({ page }, testInfo) => {
test('Confirm usage of test API (may fail if you are running your own Time Safari)', async ({ page }, testInfo) => {
// Load account view
await page.goto('./account');
await page.getByRole('heading', { name: 'Advanced' }).click();
@@ -114,33 +101,15 @@ test('Confirm test API setting (may fail if you are running your own Time Safari
test('Check User 0 can register a random person', async ({ page }) => {
await importUser(page, '00');
const newDid = await generateAndRegisterEthrUser(page);
const newDid = await generateEthrUser(page);
expect(newDid).toContain('did:ethr:');
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
await page.getByPlaceholder('What was given').fill('Gave me access!');
await page.getByPlaceholder('What was given').fill('Access!');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That gift was recorded.')).toBeVisible();
// now ensure that alert goes away
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
await expect(page.getByText('That gift was recorded.')).toBeHidden();
// now delete the contact to test that pages still do reasonable things
await deleteContact(page, newDid);
// go the activity page for this new person
await page.goto('./did/' + encodeURIComponent(newDid));
// maybe replace by: const popupPromise = page.waitForEvent('popup');
let error;
try {
await page.waitForSelector('div[role="alert"]', { timeout: 2000 });
error = new Error('Error alert should not show.');
} catch (error) {
// success
} finally {
if (error) {
throw error;
}
}
});

View File

@@ -1,32 +0,0 @@
import { test, expect } from '@playwright/test';
import { deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
test('Check User 0 can invite someone', async ({ page }) => {
const newDid = await generateNewEthrUser(page);
await importUser(page, '00');
await page.goto('./invite-one');
await page.locator('button > svg.fa-plus').click();
const neighborNum = await generateRandomString(5);
await page.getByPlaceholder('Notes', { exact: true }).fill(`Neighbor ${neighborNum}`);
// get the expiration date input and set to 14 days from now
const expirationDate = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
await page.locator('input[type="date"]').fill(expirationDate.toISOString().split('T')[0]);
await page.locator('button:has-text("Save")').click();
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
// check that the invite is in the list
const newInviteLine = page.locator(`td:has-text("Neighbor ${neighborNum}")`);
await expect(newInviteLine).toBeVisible();
// retrieve the link from the title
const inviteLink = await newInviteLine.getAttribute('data-testId');
expect(inviteLink).not.toBeNull();
// become the new user and accept the invite
await switchToUser(page, newDid);
await page.goto(inviteLink as string);
await page.getByPlaceholder('Name', { exact: true }).fill(`My pal User #0`);
await page.locator('button:has-text("Save")').click();
await expect(page.locator('button:has-text("Save")')).toBeHidden();
await expect(page.locator(`li:has-text("My pal User #0")`)).toBeVisible();
});

View File

@@ -48,9 +48,8 @@ test('Create new project, then search for it', async ({ page }) => {
// Create new project
await page.goto('./projects');
// close onboarding, but not with a click to go to the main screen
await page.locator('div > svg.fa-xmark').click();
await page.locator('button > svg.fa-plus').click();
await page.getByRole('link', { name: 'Projects', exact: true }).click();
await page.getByRole('button').click();
await page.getByPlaceholder('Idea Name').fill(finalTitle);
await page.getByPlaceholder('Description').fill(finalDescription);
await page.getByPlaceholder('Website').fill(standardWebsite);
@@ -64,6 +63,7 @@ test('Create new project, then search for it', async ({ page }) => {
// Search for newly-created project in /projects
await page.goto('./projects');
await page.getByRole('link', { name: 'Projects', exact: true }).click();
await expect(page.locator('ul#listProjects li').filter({ hasText: finalTitle })).toBeVisible();
// Search for newly-created project in /discover

View File

@@ -7,7 +7,6 @@ test('Create 10 new projects', async ({ page }) => {
// Standard texts
const standardTitle = "Idea ";
const standardDescription = "Description of Idea ";
const standardWebsite = 'https://example.com';
// Title and description arrays
const finalTitles = [];
@@ -24,34 +23,19 @@ test('Create 10 new projects', async ({ page }) => {
finalDescriptions.push(loopDescription);
}
// Set date
const today = new Date();
const oneMonthAhead = new Date(today.setDate(today.getDate() + 30));
const standardDate = oneMonthAhead.toISOString().split('T')[0];
// Set time
const now = new Date();
const futureTime = new Date(now.setHours(now.getHours() + 1));
const standardHour = futureTime.getHours().toString().padStart(2, '0');
const standardMinute = futureTime.getMinutes().toString().padStart(2, '0');
const standardTime = `${standardHour}:${standardMinute}`;
// Import user 00
await importUser(page, '00');
// Create new projects
for (let i = 0; i < projectCount; i++) {
await page.goto('./projects');
if (i === 0) {
// close onboarding, but not with a click to go to the main screen
await page.locator('div > svg.fa-xmark').click();
}
await page.locator('button > svg.fa-plus').click();
await page.getByRole('link', { name: 'Projects', exact: true }).click();
await page.getByRole('button').click();
await page.getByPlaceholder('Idea Name').fill(finalTitles[i]); // Add random suffix
await page.getByPlaceholder('Description').fill(finalDescriptions[i]);
await page.getByPlaceholder('Website').fill(standardWebsite);
await page.getByPlaceholder('Start Date').fill(standardDate);
await page.getByPlaceholder('Start Time').fill(standardTime);
await page.getByPlaceholder('Website').fill('https://example.com');
await page.getByPlaceholder('Start Date').fill('2025-12-01');
await page.getByPlaceholder('Start Time').fill('12:00');
await page.getByRole('button', { name: 'Save Project' }).click();
// Check texts

View File

@@ -19,7 +19,6 @@ test('Record something given', async ({ page }) => {
// Record something given
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
await page.getByPlaceholder('What was given').fill(finalTitle);
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());

View File

@@ -1,11 +1,11 @@
import { test, expect } from '@playwright/test';
import { importUser, createUniqueStringsArray, createRandomNumbersArray } from './testUtils';
test('Record 9 new gifts', async ({ page }) => {
const giftCount = 9; // because 10 has taken us above 30 seconds
test('Record 10 new gifts', async ({ page }) => {
const giftCount = 10;
// Standard text
const standardTitle = 'Gift ';
const standardTitle = "Gift ";
// Field value arrays
const finalTitles = [];
@@ -30,9 +30,6 @@ test('Record 9 new gifts', async ({ page }) => {
for (let i = 0; i < giftCount; i++) {
// Record something given
await page.goto('./');
if (i === 0) {
await page.getByTestId('closeOnboardingAndFinish').click();
}
await page.getByRole('heading', { name: 'Unnamed/Unknown' }).click();
await page.getByPlaceholder('What was given').fill(finalTitles[i]);
await page.getByRole('spinbutton').fill(finalNumbers[i].toString());

View File

@@ -22,7 +22,7 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
const finalTitle = standardTitle + finalRandomString;
// Contact name
const contactName = 'Contact #000 renamed';
const contactName = 'Contact #111';
// Import user 01
await importUser(page, '01');
@@ -31,7 +31,7 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
await page.goto('./contacts');
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, User #000');
await page.locator('button > svg.fa-plus').click();
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
await expect(page.locator('div[role="alert"]')).toBeVisible();
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
@@ -45,11 +45,9 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible();
await page.getByPlaceholder('Name', { exact: true }).fill(contactName);
await page.locator('.dialog > .flex > button').first().click();
// await page.locator('.dialog > .flex > button').first().click(); // close alert
// Confirm that home shows contact in "Record Something…"
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await expect(page.locator('#sectionRecordSomethingGiven ul li').filter({ hasText: contactName }).nth(0)).toBeVisible();
// Record something given by new contact
@@ -61,9 +59,6 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
// Refresh home view and check gift
await page.goto('./');
// Firefox complains on load the initial feed here when we use the test server.
// It may be similar to the CORS problem below.
await page.locator('li').filter({ hasText: finalTitle }).locator('a').click();
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
@@ -79,7 +74,6 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
// Go to home view and look for gift
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await page.locator('li').filter({ hasText: finalTitle }).locator('a').click();
// Confirm gift as user 00
@@ -94,24 +88,6 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
await expect(page.locator('div[role="alert"]')).toBeVisible();
});
test('Without being registered, add contacts without registration', async ({ page, context }) => {
await page.goto('./account');
// wait until the DID shows on the page in the 'did' element
const didElem = await page.getByTestId('didWrapper').locator('code');
const newDid = await didElem.innerText();
expect(newDid.trim()).toEqual('');
// Add new contact without registering
await page.goto('./contacts');
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39, User #111');
await page.locator('button > svg.fa-plus').click();
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
// wait for the alert to disappear, which also ensures that there is no "Register" button waiting
await expect(page.locator('div[role="alert"]')).toBeHidden();
});
test('Add contact, copy details, delete, and import various ways', async ({ page, context }) => {
await importUser(page, '00');
@@ -141,7 +117,6 @@ test('Add contact, copy details, delete, and import various ways', async ({ page
await page.getByTestId('contactCheckAllTop').click();
await page.getByTestId('copySelectedContactsButtonTop').click();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
await expect(page.locator('div[role="alert"]')).toBeHidden();
// I would prefer to copy from the clipboard, but the recommended approaches don't work.
// this seems to fail in non-chromium browsers
//await context.grantPermissions(['clipboard-read', 'clipboard-write']);
@@ -150,19 +125,12 @@ test('Add contact, copy details, delete, and import various ways', async ({ page
// see contact details on the second contact
await page.getByTestId('contactListItem').nth(1).locator('a').click();
await page.getByRole('heading', { name: 'Identifier Details' }).isVisible();
// remove contact
await page.locator('button > svg.fa-trash-can').click();
await page.locator('div[role="alert"] button:has-text("Yes")').click();
// for some reason, .isHidden() (without expect) doesn't work
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
// Firefox has a problem when we run this against the test server. It doesn't load the feed.
// It says there's a CORS problem; maybe it's more strict than the other browsers.
// It works when we set the config to use a local server.
// Seems like we hit a similar problem above.
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
await expect(page.locator('div[role="alert"]')).toBeHidden();
// go to the contacts page and paste the copied contact details
await page.goto('./contacts');
@@ -178,22 +146,4 @@ test('Add contact, copy details, delete, and import various ways', async ({ page
await page.locator('button', { hasText: 'Import' }).click();
// check that there are more contacts
await expect(page.getByTestId('contactListItem')).toHaveCount(2);
// Import via the file backup-import
await page.goto('./account');
await page.getByRole('heading', { name: 'Advanced' }).click();
const fileSelect = await page.locator('input[type="file"]')
//fileSelect.click();
fileSelect.setInputFiles('./test-playwright/exported-data.json');
await page.locator('button', { hasText: 'Import Only Contacts' }).click();
// we're on the contact-import page
await expect(page.locator('li', { hasText: '- New' })).toHaveCount(3);
await expect(page.locator('li', { hasText: '- Existing' })).toHaveCount(1);
await expect(page.locator('span').filter({ hasText: 'the same as' })).toBeHidden();
await page.locator('button', { hasText: 'Import' }).click();
// check that there are more contacts
await expect(page.getByTestId('contactListItem')).toHaveCount(5);
// The visibility error is because currently the server returns an error for the same person.
// But it should only show that one, for User #000.
});

View File

@@ -9,42 +9,35 @@ test('Record an offer', async ({ page }) => {
const updatedDescription = `Updated ${description}`;
const randomNonZeroNumber = Math.floor(Math.random() * 998) + 1;
// Switch to user 0
// Create new ID for default user
await importUser(page);
// Select a project
await page.goto('./discover');
await page.getByTestId('closeOnboardingAndFinish').click();
await page.locator('ul#listDiscoverResults li:nth-child(1)').click();
// Record an offer
await page.locator('button', { hasText: 'Edit' }).isVisible(); // since the 'edit' takes longer to show, wait for that (lest the click miss)
await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(description);
await page.getByTestId('inputOfferAmount').fill(randomNonZeroNumber.toString());
expect(page.getByRole('button', { name: 'Sign & Send' }));
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible();
// go to the offer and check the values
await page.goto('./projects');
await page.getByRole('link', { name: 'Offers', exact: true }).click();
await page.locator('li').filter({ hasText: description }).locator('a').first().click();
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
await expect(page.getByText(description, { exact: true })).toBeVisible();
await expect(page.getByText('Offered to a bigger plan')).toBeVisible();
const serverPagePromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'View on the Public Server' }).click();
const serverPage = await serverPagePromise;
await expect(serverPage.getByText(description)).toBeVisible();
await expect(serverPage.getByText('did:none:HIDDEN')).toBeVisible();
await serverPage.getByText(description);
await serverPage.getByText('did:none:HIDDEN');
// Now update that offer
// find the edit page and check the old values again
await page.goto('./projects');
await page.getByRole('link', { name: 'Offers', exact: true }).click();
await page.locator('li').filter({ hasText: description }).locator('a').first().click();
await page.getByTestId('editClaimButton').click();
await page.locator('heading', { hasText: 'What is offered' }).isVisible();
@@ -56,55 +49,15 @@ test('Record an offer', async ({ page }) => {
await itemDesc.fill(updatedDescription);
await amount.fill(String(randomNonZeroNumber + 1));
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible();
// go to the offer claim again and check the updated values
await page.goto('./projects');
await page.getByRole('link', { name: 'Offers', exact: true }).click();
await page.locator('li').filter({ hasText: description }).locator('a').first().click();
const newItemDesc = page.getByTestId('description');
const newItemDesc = await page.getByTestId('description');
await expect(newItemDesc).toHaveText(updatedDescription);
// go to edit page
await page.getByTestId('editClaimButton').click();
const newAmount = page.getByTestId('inputOfferAmount');
const newAmount = await page.getByTestId('inputOfferAmount');
await expect(newAmount).toHaveValue((randomNonZeroNumber + 1).toString());
// go to the home page and check that the offer is shown as new
await page.goto('./');
const offerNumElem = page.getByTestId('newOffersToUserProjectsActivityNumber');
await expect(offerNumElem).toHaveText('50+');
// click on the number of new offers to go to the list page
await offerNumElem.click();
await expect(page.getByText('New Offers To Your Projects', { exact: true })).toBeVisible();
// get the icon child of the showOffersToUserProjects
await page.getByTestId('showOffersToUserProjects').locator('div > svg.fa-chevron-right').click();
await expect(page.getByText(description)).toBeVisible();
});
test('Affirm delivery of an offer', async ({ page }) => {
// go to the home page and check that the offer is shown as new
await importUser(page);
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
const offerNumElem = page.getByTestId('newOffersToUserProjectsActivityNumber');
await expect(offerNumElem).toBeVisible();
// click on the number of new offers to go to the list page
await offerNumElem.click();
// get the link that comes after the showOffersToUserProjects and click it
await page.getByTestId('showOffersToUserProjects').locator('a').click();
// get the first item of the list and click on the icon with file-lines
const firstItem = page.getByTestId('listRecentOffersToUserProjects').locator('li').first();
await expect(firstItem).toBeVisible();
await firstItem.locator('svg.fa-file-lines').click();
await expect(page.getByText('Verifiable Claim Details', { exact: true })).toBeVisible();
// click on the 'Affirm Delivery' button
await page.getByRole('button', { name: 'Affirm Delivery' }).click();
// fill our offer info and submit
await page.getByPlaceholder('What was given').fill('Whatever the offer says');
await page.getByRole('spinbutton').fill('2');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That gift was recorded.')).toBeVisible();
});

View File

@@ -1,83 +0,0 @@
import { test, expect } from '@playwright/test';
import { importUser, generateNewEthrUser, switchToUser } from './testUtils';
test('New offers for another user', async ({ page }) => {
const user01Did = await generateNewEthrUser(page);
await page.goto('./');
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
await importUser(page, '00');
await page.goto('./contacts');
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(user01Did + ', A Friend');
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
await page.locator('button > svg.fa-plus').click();
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
// show buttons to make offers directly to people
await page.getByRole('button').filter({ hasText: /See Hours/i }).click();
// make an offer directly to user 1
// Generate a random string of 3 characters, skipping the "0." at the beginning
const randomString1 = Math.random().toString(36).substring(2, 5);
await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(`help of ${randomString1} from #000`);
await page.getByTestId('inputOfferAmount').fill('1');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
// make another offer to user 1
const randomString2 = Math.random().toString(36).substring(2, 5);
await page.getByTestId('offerButton').click();
await page.getByTestId('inputDescription').fill(`help of ${randomString2} from #000`);
await page.getByTestId('inputOfferAmount').fill('3');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
// as user 1, go to the home page and check that two offers are shown as new
await switchToUser(page, user01Did);
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
await expect(offerNumElem).toHaveText('2');
// click on the number of new offers to go to the list page
await offerNumElem.click();
await expect(page.getByText('New Offers To You', { exact: true })).toBeVisible();
await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click();
// note that they show in reverse chronologicalorder
await expect(page.getByText(`help of ${randomString2} from #000`)).toBeVisible();
await expect(page.getByText(`help of ${randomString1} from #000`)).toBeVisible();
// click on the latest offer to keep it as "unread"
await page.hover(`li:has-text("help of ${randomString2} from #000")`);
// await page.locator('li').filter({ hasText: `help of ${randomString2} from #000` }).click();
// await page.locator('div').filter({ hasText: /keep all above/ }).click();
// now find the "Click to keep all above as new offers" after that list item and click it
const liElem = page.locator('li').filter({ hasText: `help of ${randomString2} from #000` });
await liElem.hover();
const keepAboveAsNew = await liElem.locator('div').filter({ hasText: /keep all above/ });
await keepAboveAsNew.click();
// now see that only one offer is shown as new
await page.goto('./');
offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
await expect(offerNumElem).toHaveText('1');
await offerNumElem.click();
await expect(page.getByText('New Offer To You', { exact: true })).toBeVisible();
await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click();
// now see that no offers are shown as new
await page.goto('./');
// wait until the list with ID listLatestActivity has at least one visible item
await page.locator('#listLatestActivity li').first().waitFor({ state: 'visible' });
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
});

View File

@@ -1,8 +0,0 @@
The author disclaims copyright to this source code. In place of a legal notice, here is a blessing:
May you do good and not evil.
May you find forgiveness for yourself and forgive others.
May you share freely, never taking more than you give.
________________________________________________________________
from https://www.sqlite.org/src/info/689401a6cfb4c234 and memorialized here https://spdx.org/licenses/blessing.html

View File

@@ -1,86 +0,0 @@
{
"formatName": "dexie",
"formatVersion": 1,
"data": {
"databaseName": "TimeSafari",
"databaseVersion": 4,
"tables": [
{
"name": "contacts",
"schema": "did,name",
"rowCount": 12
},
{
"name": "logs",
"schema": "date",
"rowCount": 0
},
{
"name": "settings",
"schema": "id,&accountDid",
"rowCount": 2
},
{
"name": "temp",
"schema": "id",
"rowCount": 0
}
],
"data": [{
"tableName": "contacts",
"inbound": true,
"rows": [
{
"did": "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F",
"name": "User #00",
"publicKeyBase64": "A7Ix5zQT8dNrMyd2OtmkS7gfyRSAoUl3qzz9Mt8FZK8d",
"nextPubKeyHashB64": "d9D/wZLUvI/EyOiMKyxcml0uPKrTh5T0tMGcQjjaqE4=",
"seesMe": false
},
{
"did": "did:ethr:0x0Fc2683554C20B3Ea75aa5bf77B3519005082037",
"name": "tester",
"nextPubKeyHashB64": "CCOqpInfn4Exg7rIdiUxU+K+BUr5GQUVdSmN6SHOHKs=",
"profileImageUrl": 0,
"publicKeyBase64": "AnWEewlbkBH7Q+DZgAglXkqd3Ufxvqvf5OcjenO62Opl",
"registered": true,
"seesMe": true
},
{
"did": "did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39",
"name": "User 111",
"nextPubKeyHashB64": "ge3fGAoxP+Ak48UFg2u9BPdd4ircmvqT34p9spU+h5M=",
"profileImageUrl": "https://test-image.timesafari.app/6b3cba6970f44a883bcbaf302384c50f9b0940e4812ac188649a3b9ec0ebada9.png",
"publicKeyBase64": "A1HdQoCMRkWkTgBvTcJFT6tZ6EXIWZaa0aFsnYNzfE/L",
"registered": true,
"seesMe": true
},
{
"did": "did:peer:0zKMFjvUgYrM1hXwDciKXRoR8dd5PXWoHxAFQZ9jU46wURZizUC128RpzpEc6CpzxQWMdHVS5b3W91yGR6hLUkfcC7UdLtU5jB2fW5TMrQTUte",
"name": "did:peer ixroo",
"publicKeyBase64": 0,
"nextPubKeyHashB64": 0,
"registered": true,
"seesMe": true
}
]
},{
"tableName": "settings",
"inbound": true,
"rows": [
{
"id": 1,
"activeDid": "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F",
"apiServer": "https://test-api.endorser.ch",
"lastViewedClaimId": "01J7SCRCJBYHS3RQWFP9EHXYJZ",
"firstName": "Me"
},
{
"isRegistered": false,
"accountDid": "did:ethr:0x0Fc2683554C20B3Ea75aa5bf77B3519005082037",
"id": 2
}
]
}]
}
}

View File

@@ -34,62 +34,35 @@ export async function importUser(page: Page, id?: string): Promise<string> {
// This is to switch to someone already in the identity table. It doesn't include registration.
export async function switchToUser(page: Page, did: string): Promise<void> {
// This is the direct approach but users have to tap on things so we'll do that instead.
//await page.goto('./identity-switcher');
await page.goto('./account');
await page.getByRole('heading', { name: 'Advanced' }).click();
await page.getByRole('link', { name: 'Switch Identifier' }).click();
const didElem = await page.locator(`code:has-text("${did}")`);
await didElem.isVisible();
await didElem.click();
// wait for the switch to happen and the account page to fully load
await page.getByTestId('didWrapper').locator('code:has-text("did:")');
await page.getByRole('code', { name: did }).click();
}
function createContactName(did: string): string {
return "User " + did.slice(11, 14);
}
export async function deleteContact(page: Page, did: string): Promise<void> {
await page.goto('./contacts');
const contactName = createContactName(did);
// go to the detail page for this contact
await page.locator(`li[data-testid="contactListItem"] h2:has-text("${contactName}") + a`).click();
// delete the contact
await page.locator('button > svg.fa-trash-can').click();
await page.locator('div[role="alert"] button:has-text("Yes")').click();
// for some reason, .isHidden() (without expect) doesn't work
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
}
export async function generateNewEthrUser(page: Page): Promise<string> {
// Generate a new random user and register them.
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
export async function generateEthrUser(page: Page): Promise<string> {
await page.goto('./start');
await page.getByTestId('newSeed').click();
await expect(page.locator('span:has-text("Created")')).toBeVisible();
await page.goto('./account');
// wait until the DID shows on the page in the 'did' element
const didElem = await page.getByTestId('didWrapper').locator('code:has-text("did:")');
const newDid = await didElem.innerText();
return newDid;
}
// Generate a new random user and register them.
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
const newDid = await generateNewEthrUser(page);
await importUser(page, '000'); // switch to user 000
await page.goto('./contacts');
const contactName = createContactName(newDid);
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`);
const threeChars = newDid.slice(11, 14);
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, User ${threeChars}`);
await page.locator('button > svg.fa-plus').click();
await page.locator('li', { hasText: threeChars }).click();
// register them
await page.locator('div[role="alert"] button:has-text("Yes")').click();
// wait for it to disappear because the next steps may depend on alerts being gone
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
await expect(page.locator('li', { hasText: contactName })).toBeVisible();
return newDid;
}
@@ -122,4 +95,4 @@ export async function createRandomNumbersArray(count: number): Promise<number[]>
}
return numbersArray;
}
}