Compare commits

...

183 Commits

Author SHA1 Message Date
f269f5fa77 Merge branch 'master' into no-accounts-in-memory 2023-07-18 06:41:44 -04:00
Matthew Raymer
e8eae544f3 Merge remote-tracking branch 'origin/no-accounts-in-memory' 2023-07-18 18:50:55 +08:00
Jose Olarte III
34636d6047 Fixed alert message visibility 2023-07-18 17:14:03 +08:00
91b46eaaee Update 'project.task.yaml' 2023-07-18 02:15:04 -04:00
31d1a449ae Update 'project.task.yaml' 2023-07-18 02:12:40 -04:00
1248132076 Update 'project.task.yaml' 2023-07-18 02:12:29 -04:00
015704c94e Merge pull request 'home-gifting-improvements' (#43) from home-gifting-improvements into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#43
2023-07-18 01:35:40 -04:00
540ef916c2 Merge pull request 'Remove form tags' (#44) from form-tag-cleanup into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#44
2023-07-18 01:35:22 -04:00
bee7c87a8f Merge branch 'master' into no-accounts-in-memory 2023-07-17 16:16:44 -04:00
6bbc88f86c avoid console errors when no identity exists, and add to tasks 2023-07-14 20:54:26 -06:00
624abbb179 correct an error with unseen internal methods 2023-07-14 20:53:41 -06:00
110ed009b2 remove unused reference, call out another verbiage fix to do 2023-07-14 20:30:40 -06:00
a5892238d5 on tasks: prioritize notifications, add fixes 2023-07-14 20:25:15 -06:00
8eb80a9ede remove unnecessary async (that also happens to break display) 2023-07-14 20:11:04 -06:00
Jose Olarte III
32125133f0 No-contacts state 2023-07-14 21:33:56 +08:00
Matthew Raymer
47ade49e31 Cleanup after some testing 2023-07-14 20:29:38 +08:00
Jose Olarte III
47ce91cca1 Implemented design of Contact Gifting List view 2023-07-14 19:42:13 +08:00
Jose Olarte III
3e52b504b0 Polished gifted dialog UI 2023-07-14 18:27:43 +08:00
Matthew Raymer
4ecea1ab0e Add a special page for seeing all contacts to gift 2023-07-14 18:12:39 +08:00
Matthew Raymer
b9fdc920ea Remove the old identity management. Update project tasks 2023-07-13 18:13:41 +08:00
Matthew Aaron Raymer
0907d59a6a Remove form tags 2023-07-13 16:06:09 +08:00
59ce15c744 Merge pull request 'Adding Identity Management stubs' (#42) from identity-switcher into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#42
2023-07-12 22:49:58 -04:00
Jose Olarte III
9960a96a20 Design tweaks to Latest Activity 2023-07-12 23:17:28 +08:00
Jose Olarte III
098c6c0fa0 Compacted Quick Action section 2023-07-12 22:52:58 +08:00
Jose Olarte III
ead37ede74 Added back button 2023-07-12 19:41:05 +08:00
Matthew Raymer
f428199228 Redirect to account tab after switching identity 2023-07-12 19:12:31 +08:00
Matthew Raymer
1405b88323 Functional Identity Management 2023-07-12 18:47:21 +08:00
Jose Olarte III
44fc2850dd UL-based identity list + markup fixes 2023-07-12 16:48:30 +08:00
Matthew Raymer
52d411470e Send back to Jose for some list magic 2023-07-12 16:11:35 +08:00
Jose Olarte III
ab678a900a Added static HTML to Account Switcher view 2023-07-12 15:45:10 +08:00
Jose Olarte III
efa59e170f Tweaked router link styles 2023-07-12 15:27:59 +08:00
Matthew Raymer
7a4ceaa455 Adding Identity Management stubs 2023-07-12 15:16:07 +08:00
d7a9fb6d54 Merge branch 'master' into no-accounts-in-memory 2023-07-11 22:57:20 -04:00
78b98bab5e Merge pull request 'fix local & remote search to return the same results, fix beforeId check, fix handleId reference' (#41) from similarify-search into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#41
2023-07-11 22:36:56 -04:00
2493f2ad39 fix local & remote search to work, fix beforeId check, fix handleId setting 2023-07-11 21:16:57 -06:00
cf2b80b1f5 Merge branch 'master' into no-accounts-in-memory 2023-07-11 21:28:51 -04:00
00954693b5 Merge pull request 'Integrating InfiniteScroll to the Discovery view' (#39) from discover-infinite-scroll into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#39
2023-07-11 21:19:49 -04:00
2dd77f898f Merge branch 'master' into discover-infinite-scroll 2023-07-11 21:19:20 -04:00
c1f218c2f3 Update 'project.task.yaml' 2023-07-11 21:10:33 -04:00
b5e78e5dc8 Update 'project.task.yaml' 2023-07-11 21:03:39 -04:00
b86323ec83 adjust didInfo so we can use DIDs and not identities, removing last of identities in memory 2023-07-11 15:30:51 -06:00
8add6448fb remove code that keeps the private key (account) data in memory 2023-07-11 09:10:25 -06:00
47442655cb update tasks 2023-07-11 08:40:44 -06:00
Matthew Raymer
1d362c314b Beginning of integration. Seems the data coming back from local and remote are different? 2023-07-11 18:31:58 +08:00
3eda246e85 Merge pull request 'DiscoverView searches almost done.' (#38) from discover-view-etc into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#38
2023-07-11 01:36:19 -04:00
Matthew Raymer
3f13d3ea33 CLeanup header creation 2023-07-10 19:03:20 +08:00
Matthew Raymer
cef346e487 Move almost all interfaces to endorserServer.ts 2023-07-10 18:45:50 +08:00
Matthew Raymer
fed23a61ee Remove HelloWorld and do sweeping 2023-07-10 18:20:13 +08:00
Matthew Raymer
b6b7c56157 DiscoverView searches almost done.
Contact fixes
ContactAmounts fix quick-nav
Cleaning up ProjectView still more to do
Hide advanced by default on StatisticsView
project.task updated
2023-07-10 18:03:51 +08:00
3f8be3b4de Merge pull request 'Show the gives to & from a project (plan)' (#37) from project-gives into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#37
2023-07-10 00:39:01 -04:00
21af37c2c2 Merge pull request 'move QR for contact up, right under header' (#36) from move-qr-up into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#36
2023-07-10 00:38:51 -04:00
0b7a35c9b8 list the gives to a plan and gives to which this plan contributed 2023-07-09 20:37:08 -06:00
0257901c5b allow viewing of a project without an ID (and other refactors) 2023-07-09 09:38:28 -06:00
d9d6096275 consolidate the "gave" actions on a projecct 2023-07-09 08:31:11 -06:00
ed7d37c649 move QR for contact up, right under header 2023-07-09 08:15:51 -06:00
81dd6eb595 Merge pull request 'Updates to contacts UI. Sweep for buildIdentity and buildHeaders' (#35) from contacts-identity into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#35
2023-07-08 22:58:51 -04:00
Matthew Raymer
c61bb88788 More cleanup from project task list 2023-07-08 18:42:53 +08:00
Matthew Raymer
3bd55f3ad2 More cleanup and application of new db loading 2023-07-08 18:31:12 +08:00
Matthew Raymer
3471afdf25 Cleaned up messages and improved the accountsDB lookups 2023-07-08 17:39:20 +08:00
Matthew Raymer
e25a83ff1b Move account counter to mounted event 2023-07-08 17:19:52 +08:00
Matthew Raymer
0fbdb45d3e Project Task update 2023-07-07 20:33:43 +08:00
Matthew Raymer
dc23ba1375 Fix a bug in HomeView and clean up recordGive method 2023-07-07 18:43:16 +08:00
Matthew Raymer
08137eb000 Updates to contacts UI. Sweep for buildIdentity and buildHeaders 2023-07-07 18:28:06 +08:00
Matthew Raymer
5d49965166 Simple fix. Missing reference to QuickNav 2023-07-07 16:22:53 +08:00
8e8aa4356d Update 'project.task.yaml' 2023-07-06 22:20:43 -04:00
59a354027e Update 'src/views/StatisticsView.vue'
Clean up spurious comment.
2023-07-06 21:36:10 -04:00
Matthew Raymer
5dc80ce12a A bit more cleanup. Problem in Contacts to resolve. 2023-07-06 18:33:13 +08:00
Matthew Raymer
754bced2a9 Considerable cleanup. I think I also found the issue from the other day with values not loading from settings. 2023-07-06 18:12:21 +08:00
Matthew Raymer
e3f58bd593 Purge all vue-class-component with vue-facing-decorator.
Make some strike-throughs for project-task
Update package.json
2023-07-06 17:28:08 +08:00
Matthew Raymer
3b41014083 Considerable cleanup and merge 2023-07-06 16:59:50 +08:00
f568149745 Merge pull request 'allow choice of no identity (for testing)' (#32) from choose-no-id into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#32
2023-07-06 02:40:11 -04:00
a27d035e9b Merge branch 'master' into choose-no-id 2023-07-06 02:40:00 -04:00
16d0be681c Merge pull request 'quicknav-component' (#34) from quicknav-component into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#34
2023-07-06 02:38:19 -04:00
Matthew Raymer
5be67fd4c9 Rolled out to all views that had HTML quicknav 2023-07-05 17:59:31 +08:00
Matthew Raymer
dda3ad057d This is the QuickNav component 2023-07-05 17:58:18 +08:00
Matthew Raymer
cf54096326 Looks like GiftedDialog works? A little cleanup. 2023-07-05 16:27:21 +08:00
49c3971cf2 Merge pull request 'First draft of Vue3 version. WIll finish after error-logging merge' (#31) from gifted-dialog-conversion into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#31
2023-07-05 03:07:35 -04:00
Matthew Raymer
80a1185faa Fix for missing scene-container 2023-07-05 15:59:58 +08:00
cd8bc73bac Merge branch 'master' into gifted-dialog-conversion 2023-07-05 03:07:09 -04:00
e42b3ff11d allow choice of no identity (for testing) 2023-07-04 13:15:52 -06:00
d98e95915b Merge pull request 'error-logging' (#29) from error-logging into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#29
2023-07-04 12:15:24 -04:00
Matthew Raymer
4758a740de First draft of Vue3 version. WIll finish after error-logging merge 2023-07-04 20:03:36 +08:00
Matthew Raymer
0a020a4069 Clean up vestigle old alert code 2023-07-04 19:29:12 +08:00
Matthew Raymer
c859778832 Add AlertMessage component 2023-07-04 19:27:57 +08:00
Matthew Raymer
c24022c41c Make the visibility of the Alert indirect 2023-07-04 19:26:05 +08:00
Matthew Raymer
0fd4b86a84 A bit of linting 2023-07-03 21:06:24 +08:00
Matthew Raymer
c31445865e Straggler 2023-07-03 21:05:45 +08:00
Matthew Raymer
0af03227a6 Propagated AlertMessage component 2023-07-03 21:04:53 +08:00
Matthew Raymer
3c977a1f28 First roll out of an AlertMessage control 2023-07-03 20:24:23 +08:00
Matthew Raymer
8d8635a3e6 Added try...catch and linted 2023-07-03 19:44:41 +08:00
Matthew Raymer
bcc6de6fc0 New AlertMessage component 2023-07-03 19:40:53 +08:00
Matthew Raymer
99ea161da0 Removed created() and relocated to mounted() could not replicate issue 2023-07-03 18:48:01 +08:00
Matthew Raymer
3f6dbdebef Moved checkLimits inside try...catch since it has an identity check 2023-07-03 18:46:23 +08:00
Matthew Raymer
b139957e3e Added identity check 2023-07-03 18:44:42 +08:00
6e4f6d090a Merge pull request 'add project search (which just goes to console for now)' (#28) from search-projects into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#28
2023-07-02 20:21:35 -04:00
48227e8cf2 Merge branch 'master' into search-projects 2023-07-02 20:21:19 -04:00
09f02ca4b2 Merge pull request 'Quick links to record a gift, to this person or to a project' (#27) from give-to-project into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#27
2023-07-02 20:20:51 -04:00
9b3823ef0e Merge branch 'master' into give-to-project 2023-07-02 20:06:11 -04:00
cdeece1795 add project search (which just goes to console for now) 2023-07-02 07:55:45 -06:00
c2ebaa0a76 mark the give-to-project task as complete 2023-07-02 06:46:31 -06:00
3f60051599 Merge branch 'master' into give-to-project 2023-07-01 22:33:23 -06:00
a8f1e25986 add error messages for gives 2023-07-01 22:20:10 -06:00
964248e895 Update 'project.task.yaml' 2023-07-01 21:04:06 -04:00
a2b3cebdb3 finish contact selection for gives 2023-07-01 15:45:30 -06:00
bc6e52774c update tasks 2023-07-01 07:13:39 -06:00
643f777d10 start linking gives to projects 2023-07-01 07:07:46 -06:00
ec1d8404ca Merge pull request 'start a walk-through of the test' (#26) from scenarios into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#26
2023-07-01 08:12:55 -04:00
Jose Olarte III
1d6241abbb Various changes
- Loading animation in Projects view
- Per item icon + spacing fixes in Home view
2023-07-01 16:04:32 +08:00
c40b690878 fix load-project function 2023-06-28 20:28:14 -06:00
c9c81f1e5c fix wording in list of contacts, remove console.log 2023-06-28 20:20:06 -06:00
a94069e70a update tasks from previous conversation, marking milestone:2 done 2023-06-27 20:54:07 -06:00
Matthew Raymer
53f42e1ad3 Updates to fix merge and remove console.log 2023-06-27 20:07:38 +08:00
Matthew Raymer
5f0bbccbe6 Merge branch 'infinitescroll-test' 2023-06-27 19:06:41 +08:00
Matthew Raymer
3ec9056901 Merging remote master into local 2023-06-27 19:06:25 +08:00
Matthew Raymer
6d3ab7c313 Added loading settings (no image yet) plus refactored types a little more. 2023-06-27 18:57:36 +08:00
d9e9a7b740 start a walk-through 2023-06-26 20:00:23 -06:00
ea95382fdf change jwtId to rowid for paging within a derivative/cached table 2023-06-26 19:20:02 -06:00
072b663ec9 change comment whitespace to get past lint errors 2023-06-26 19:18:38 -06:00
Matthew Raymer
6393a20e7e Documentation added 2023-06-26 20:45:13 +08:00
Matthew Raymer
19d934eb28 Do a merge 2023-06-26 20:02:56 +08:00
Matthew Raymer
49ce7d43b0 Refactored and streamlined code. Still having an issue with beforeId? 2023-06-26 19:53:28 +08:00
Matthew Aaron Raymer
6233189a49 Merging next-iteration 2023-06-26 13:59:47 +08:00
Matthew Aaron Raymer
ffb0f2d37f Merge branch 'master' of ssh://173.199.124.46:222/trent_larson/kick-starter-for-time-pwa into allow-new 2023-06-26 13:49:59 +08:00
502352ad36 Update 'project.task.yaml' 2023-06-25 21:08:17 -04:00
db7b3fff06 mark another task off (no code changes) 2023-06-25 17:22:47 -06:00
bd75802a0c mark another task fixed (no code changes) 2023-06-25 17:22:06 -06:00
1a86730354 make display on creation page look halfway decent, and switch fully to it 2023-06-25 17:20:22 -06:00
a96728bec5 add better messaging on registration failure 2023-06-25 16:41:32 -06:00
944b0ad759 fix registration, separate ID creation to allow new random ones, and refactor warning and other verbiage 2023-06-24 21:07:21 -06:00
42bf34f549 fix infinitescroll to work off correct ID 2023-06-24 06:45:03 -06:00
Matthew Raymer
071c41b70c Firing properly but no data returned on extended fetch 2023-06-24 17:47:18 +08:00
5747404fd6 add marker in feed to show where they've seen claims, plus other small clean-up 2023-06-23 20:17:54 -06:00
639f630436 remove old texture image that's been replaced 2023-06-23 17:37:09 -06:00
a8794be2ea sllow quick gifting all the way to the server, maybe with hours 2023-06-23 17:00:20 -06:00
Matthew Raymer
0726a8d3ba New modules and InfiniteScroll init 2023-06-23 18:56:26 +08:00
aa2f484a9f add the other activity envisioned on the home page (though not sending data yet) 2023-06-22 20:51:06 -06:00
07e7a70d56 in feed: add token for authorized request, plus better descriptions 2023-06-12 20:29:55 -06:00
6daa515d19 load feed of give records on home screen 2023-06-11 20:37:34 -06:00
d5336dbf1b docs & comments & labels 2023-06-11 17:02:20 -06:00
b0fc8818ee add timing & animation details to stats-world 2023-05-28 09:11:37 -06:00
9f49234179 update help instructions for importing another identity 2023-05-28 07:21:35 -06:00
32351b07b7 prefer console.error messages for errors 2023-05-28 07:10:00 -06:00
0ce06bd9ac fix a problem with test data (divide by 0), and map full addresses to points 2023-05-27 17:48:41 -06:00
c3c16fd15b Merge pull request 'add QR code for contact info' (#22) from contact-qr into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#22
2023-05-22 17:27:50 -04:00
a16c34d4ee update project tasks 2023-05-21 06:59:48 -06:00
40f9de0609 add QR code for contact info 2023-05-20 20:41:15 -06:00
d1194297ac Merge branch 'switching-servers' 2023-05-20 17:34:22 -06:00
6d67a3e8e5 Merge branch '3d-world' 2023-05-20 17:19:27 -06:00
b0ccd84b62 Merge pull request 'add page to show mnemonic seed phrase for backup' (#20) from seed-backup into switching-servers
Reviewed-on: trent_larson/kick-starter-for-time-pwa#20
2023-05-20 00:47:22 -04:00
b94e36ef3b Merge pull request 'allow to switch the server' (#19) from switching-servers into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#19
2023-05-20 00:47:11 -04:00
0fed104be1 Merge branch 'master' into 3d-world 2023-05-20 00:46:30 -04:00
aa34a2362f update tasks (and rename file so the format is obvious) 2023-05-19 16:40:41 -06:00
55b53955fc add page to show mnemonic seed phrase for backup 2023-05-19 15:49:34 -06:00
fc7c1187e8 allow to switch the server 2023-05-19 13:28:52 -06:00
2feea0d645 remove console.log 2023-05-19 08:11:34 -06:00
8f3a11bb98 in stats-world, fix the location computations to be based on Give attributes 2023-05-18 20:34:57 -06:00
beb7821f58 in stats-world, refactor claim-loading code to separate file (no logic changes) 2023-05-18 18:54:11 -06:00
712b25bc71 add error message to stats page 2023-05-18 18:23:17 -06:00
f039f98b61 on stats-world: adjust some lighting and position, and add an attempt at a screen-capture 2023-05-18 18:04:10 -06:00
f7a149444a tweak timing & spacing of stats-world, and log relevant tasks 2023-05-17 21:13:01 -06:00
58e962a3bd load stats-world bushes and make 'em grow! 2023-05-17 20:46:40 -06:00
7160aa3cc5 remove test cube from stats-world 2023-05-17 18:22:46 -06:00
786f0bd94a make the lights wait to turn on in stats-world 2023-05-17 18:20:55 -06:00
b5db2b4140 refactor variable names (no logic changes) 2023-05-17 17:42:26 -06:00
faa7959929 make all lights move in stats-world 2023-05-17 17:40:10 -06:00
3dd1b6f6f0 add some other screen actions (kinda ugly); add load of claims 2023-05-17 08:46:10 -06:00
30b8b941ae Merge pull request 'copy message & infinite scroll start' (#17) from hack-copy into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#17
2023-05-17 02:32:12 -04:00
9b1c51ba15 adjust stats-world perspective & size & colors 2023-05-16 18:56:50 -06:00
8c6c32ed20 finish removal from last commit 2023-05-16 16:40:55 -06:00
0eaf72b83b remove yet more randomness that has no effect in stats-world 2023-05-16 16:30:33 -06:00
41d3ad56f5 remove some random numbers that don't seem to make a difference in stats-world 2023-05-16 16:26:27 -06:00
0227d32f15 add a simple box in stats-world 2023-05-16 16:25:32 -06:00
b5ab485354 enable zooming on the stats-world 2023-05-16 16:17:34 -06:00
02ae78de7b totally remove code to make terrain bob up and down 2023-05-16 07:50:26 -06:00
64f3dbd138 play with sizing, and stop bobbing and rotation 2023-05-16 07:49:44 -06:00
f603882d42 modify some things to remove warnings on server and in browser console 2023-05-16 06:39:36 -06:00
a9844e6e78 add beginning of visualization for statistics, unmodified from blog code 2023-05-16 06:22:19 -06:00
e4f3f9b2e0 remove extra logging 2023-05-15 16:45:22 -06:00
d7d53a5b8c refactor & shorten the 'copied' message display logic 2023-05-14 20:38:27 -06:00
44ed39b5c1 eliminate extra code for quick-message-display 2023-05-12 21:40:10 -06:00
Matthew Aaron Raymer
0dbc018c8d A bit of infinity and hack for copy message 2023-05-13 11:19:53 +08:00
fb7d51ac4c Merge pull request 'add help blurb for when we don't see info about someone that we should see' (#16) from help-visibility into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#16
2023-05-11 21:21:44 -04:00
85031f84c0 add help blurb for when we don't see info about someone that we should see 2023-05-07 19:52:40 -06:00
61 changed files with 7802 additions and 4647 deletions

View File

@@ -20,11 +20,6 @@ npm run build
npm run lint
```
### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start
(because it'll generate a new one automatically if you start on the `/account` page).
### Test key contents
See [this page](openssl_signing_console.rst)
@@ -59,17 +54,35 @@ playing one of two ways:
### Create multiple identifiers
Go to /import-account and import a new one. Then switch identifiers on the
bottom of the Your Identity page.
Go to /start and create or import a new one. Then switch identifiers on the bottom of the Your Identity page.
### Create keys with alternate tools
See [this page](openssl_signing_console.rst)
### Customize configuration
### Customize Vue configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Scenarios
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start
(because it'll generate a new one automatically if you start on the `/account` page).
## Dependencies
See https://tea.xyz
@@ -81,6 +94,8 @@ See https://tea.xyz
## Other
### Reference Material
```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
@@ -183,3 +198,12 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, [])
}
```
## Kudos
Gifts make the world go 'round!
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)

6287
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,50 +12,53 @@
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@pvermeer/dexie-encrypted-addon": "^2.0.5",
"@veramo/core": "^5.1.2",
"@veramo/credential-w3c": "^5.1.4",
"@veramo/data-store": "^5.1.2",
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0",
"@veramo/core": "^5.2.0",
"@veramo/credential-w3c": "^5.2.0",
"@veramo/data-store": "^5.2.0",
"@veramo/did-manager": "^5.1.2",
"@veramo/did-provider-ethr": "^5.1.2",
"@veramo/did-resolver": "^5.1.2",
"@veramo/did-resolver": "^5.2.0",
"@veramo/key-manager": "^5.1.2",
"@vueuse/core": "^9.13.0",
"@vueuse/core": "^10.2.1",
"@zxing/text-encoding": "^0.9.0",
"axios": "^1.3.4",
"axios": "^1.4.0",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1",
"core-js": "^3.29.1",
"dexie": "^3.2.3",
"core-js": "^3.31.1",
"dexie": "^3.2.4",
"dexie-export-import": "^4.0.7",
"did-jwt": "^6.11.5",
"ethereum-cryptography": "^1.2.0",
"did-jwt": "^7.2.4",
"ethereum-cryptography": "^2.0.0",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0",
"js-generate-password": "^0.1.7",
"js-generate-password": "^0.1.9",
"localstorage-slim": "^2.4.0",
"luxon": "^3.3.0",
"merkletreejs": "^0.3.9",
"merkletreejs": "^0.3.10",
"moment": "^2.29.4",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0",
"ramda": "^0.28.0",
"readable-stream": "^4.3.0",
"qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.0",
"readable-stream": "^4.4.2",
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2",
"vue": "^3.2.47",
"three": "^0.154.0",
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-class-component": "^8.0.0-0",
"vue-facing-decorator": "^2.1.19",
"vue-facing-decorator": "^2.1.20",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.1.6",
"web-did-resolver": "^2.0.22"
"vue-router": "^4.2.3",
"web-did-resolver": "^2.0.27"
},
"devDependencies": {
"@types/ramda": "^0.28.23",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"@types/ramda": "^0.29.3",
"@types/three": "^0.152.1",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8",
@@ -63,15 +66,15 @@
"@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.37.0",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.10.0",
"postcss": "^8.4.21",
"prettier": "^2.8.7",
"tailwindcss": "^3.3.1",
"typescript": "~5.0.3"
"eslint-plugin-prettier": "^5.0.0-alpha.1",
"eslint-plugin-vue": "^9.15.1",
"postcss": "^8.4.24",
"prettier": "^3.0.0",
"tailwindcss": "^3.3.2",
"typescript": "~5.1.6"
}
}

101
project.task.yaml Normal file
View File

@@ -0,0 +1,101 @@
tasks:
- 01 add a location for a project via map pin
- 04 search by a bounding box for local projects (see API by clicking on "Nearby")
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:jose
- 02 Fix images on projectview - allow choice of image from a pallete of images or a url image.
- 08 Scan QR code to import into contacts.
- 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data
- refactor UI :
- .5 Alerts show at the top and can be missed if you've scrolled down on the page, eg. account data download
- .2 Make alerts at the top more visible (because they're currently a similar color and sometimes aren't seen)
- Show pop-up or some message confirming that settings & contacts download has been initiated/finished
- Ensure each action sent to the server has a confirmation - eg registration
- Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery")
- 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
- 24 Move to Vite
- .5 remove edit from project page for projects owned by others
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
- .2 there are three dots at the top of ProjectViewView that refreshes the page but doesn't do anything else
- 01 fix images on project page, on discovery page
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?)
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
- Discuss whether the remaining tasks are worthwhile before MVP release.
- contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 show error to user when adding a duplicate contact
- 01 parse input more robustly (with CSV lib and not commas)
- stats v1 :
- 01 show numeric stats
- 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages
- .5 customize favicon
- 04 allow user to download claims, mine + ones I can see about me from others
- Do we want to combine first name & last name?
- Show a warning if both giver and recipient are the same (but still allow?)
- Release Minimum Viable Product :
- 08 thorough testing for errors & edge cases
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Rename DB to TimeSafari.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- linking between projects or plans :
- show total time given to & from a project
- terminology:
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
- Stats :
- 01 point out user's location on the world
- 01 present a credential selected from the stats
- 04 show gives spreading to other places
- badge for most gives/receives/confirms per day/week/month
- badge for amount given/offered to your project
- set a goal of given/offers
- automated tests, eg. cypress
- Notifications (wake on the phone, push notifications)
- Connect with phone contacts
- Multiple identities
- Peer DID
- DIDComm
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
- Do we want split first name & last name?
- 40 notifications v+ :
- pull, w/ scheduled runs
log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27

View File

@@ -1,46 +0,0 @@
- top screens from img/screens.pdf milestone:2 :
- view all :
- add infinite scroll assignee:matthew
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- replace user-affecting console.logs with error messages (eg. catches)
- contacts v1 :
- produce a video assignee:trent
- .2 warn about amounts when you cannot see them
- .1 remove 'copy' until it works
- .5 switch to prod server
- .5 Add page to show seed.
- 01 Provide a way to import the non-sensitive data.
- 01 Provide way to share your contact info.
- .2 move all "identity" references to temporary account access
- .5 make deploy for give-only features
- .5 get 'copy' to work on account page
- contacts v+ :
- .5 make advanced "show/hide amounts" button into a nice UI toggle
- .2 show error to user when adding a duplicate contact
- parse input more robustly (with CSV lib and not commas)
- refactor UI :
- .5 Alerts show at the top and can be missed, eg. account data download
- 01 Change alerts into a component (to cut down duplicate code)
- 01 Code for "nav" tabs across the bottom is duplicated on each page.
- .2 Add "copied" feedback when they click "copy" on /account
- .5 Fix how icons show on top of bottom bar on ContactAmounts page
- commit screen
- discover screen
- backup all data
- Next Viable Product afterward
- Connect with phone contacts
- Multiple identities
- Peer DID
- DIDComm

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

View File

@@ -0,0 +1,11 @@
Model Information:
* title: Lupine Plant
* source: https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439
* author: rufusrockwell (https://sketchfab.com/rufusrockwell)
Model License:
* license type: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
* requirements: Author must be credited. Commercial use is allowed.
If you use this 3D model in your project be sure to copy paste this credit wherever you share it:
This work is based on "Lupine Plant" (https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) by rufusrockwell (https://sketchfab.com/rufusrockwell) licensed under CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)

Binary file not shown.

View File

@@ -0,0 +1,229 @@
{
"accessors": [
{
"bufferView": 2,
"componentType": 5126,
"count": 2759,
"max": [
41.3074951171875,
40.37548828125,
87.85917663574219
],
"min": [
-35.245540618896484,
-36.895416259765625,
-0.9094290137290955
],
"type": "VEC3"
},
{
"bufferView": 2,
"byteOffset": 33108,
"componentType": 5126,
"count": 2759,
"max": [
0.9999382495880127,
0.9986748695373535,
0.9985831379890442
],
"min": [
-0.9998949766159058,
-0.9975876212120056,
-0.411094069480896
],
"type": "VEC3"
},
{
"bufferView": 3,
"componentType": 5126,
"count": 2759,
"max": [
0.9987699389457703,
0.9998998045921326,
0.9577858448028564,
1.0
],
"min": [
-0.9987726807594299,
-0.9990445971488953,
-0.999801516532898,
1.0
],
"type": "VEC4"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 2759,
"max": [
1.0061479806900024,
0.9993550181388855
],
"min": [
0.00279300007969141,
0.0011620000004768372
],
"type": "VEC2"
},
{
"bufferView": 0,
"componentType": 5125,
"count": 6378,
"type": "SCALAR"
}
],
"asset": {
"extras": {
"author": "rufusrockwell (https://sketchfab.com/rufusrockwell)",
"license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
"source": "https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439",
"title": "Lupine Plant"
},
"generator": "Sketchfab-12.68.0",
"version": "2.0"
},
"bufferViews": [
{
"buffer": 0,
"byteLength": 25512,
"name": "floatBufferViews",
"target": 34963
},
{
"buffer": 0,
"byteLength": 22072,
"byteOffset": 25512,
"byteStride": 8,
"name": "floatBufferViews",
"target": 34962
},
{
"buffer": 0,
"byteLength": 66216,
"byteOffset": 47584,
"byteStride": 12,
"name": "floatBufferViews",
"target": 34962
},
{
"buffer": 0,
"byteLength": 44144,
"byteOffset": 113800,
"byteStride": 16,
"name": "floatBufferViews",
"target": 34962
}
],
"buffers": [
{
"byteLength": 157944,
"uri": "scene.bin"
}
],
"images": [
{
"uri": "textures/lambert2SG_baseColor.png"
},
{
"uri": "textures/lambert2SG_normal.png"
}
],
"materials": [
{
"alphaCutoff": 0.2,
"alphaMode": "MASK",
"doubleSided": true,
"name": "lambert2SG",
"normalTexture": {
"index": 1
},
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicFactor": 0.0
}
}
],
"meshes": [
{
"name": "Object_0",
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 0,
"TANGENT": 2,
"TEXCOORD_0": 3
},
"indices": 4,
"material": 0,
"mode": 4
}
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
2.220446049250313e-16,
-1.0,
0.0,
0.0,
1.0,
2.220446049250313e-16,
0.0,
0.0,
0.0,
0.0,
1.0
],
"name": "Sketchfab_model"
},
{
"children": [
2
],
"name": "LupineSF.obj.cleaner.materialmerger.gles"
},
{
"mesh": 0,
"name": "Object_2"
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9987,
"wrapS": 10497,
"wrapT": 10497
}
],
"scene": 0,
"scenes": [
{
"name": "Sketchfab_Scene",
"nodes": [
0
]
}
],
"textures": [
{
"sampler": 0,
"source": 0
},
{
"sampler": 0,
"source": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

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

View File

@@ -0,0 +1,129 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giver?.name || "somebody not specified" }}
</h1>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received"
v-model="description"
/>
<div class="flex flex-row mb-6">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2"
>Hours</span
>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()"
>
<fa icon="chevron-left" />
</div>
<input
type="text"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="hours"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<fa icon="chevron-right" />
</div>
</div>
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
<button
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="cancel"
>
Cancel
</button>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
@Component
export default class GiftedDialog extends Vue {
@Prop message = "";
giver = null;
description = "";
hours = "0";
visible = false;
open(giver) {
// giver: GiverInputInfo
this.giver = giver;
this.visible = true;
}
close() {
this.visible = false;
}
increment() {
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
}
decrement() {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
}
@Emit("dialog-result")
confirm() {
const result = {
action: "confirm",
giver: this.giver,
hours: parseFloat(this.hours),
description: this.description,
};
this.close();
this.description = "";
this.giver = null;
this.hours = "0";
return result;
}
@Emit("dialog-result")
cancel() {
const result = { action: "cancel" };
this.close();
return result;
}
}
</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

@@ -1,150 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
>vue-cli documentation</a
>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
target="_blank"
rel="noopener"
>babel</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa"
target="_blank"
rel="noopener"
>pwa</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
target="_blank"
rel="noopener"
>router</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
target="_blank"
rel="noopener"
>vuex</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
target="_blank"
rel="noopener"
>eslint</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
target="_blank"
rel="noopener"
>typescript</a
>
</li>
</ul>
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
>Forum</a
>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
>Community Chat</a
>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
>Twitter</a
>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
>vue-router</a
>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a
>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
props: {
msg: String,
},
})
export default class HelloWorld extends Vue {
msg!: string;
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div ref="scrollContainer">
<slot />
<div ref="sentinel" style="height: 1px"></div>
</div>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
@Component
export default class InfiniteScroll extends Vue {
@Prop({ default: 200 })
readonly distance!: number;
private observer!: IntersectionObserver;
private isInitialRender = true;
updated() {
if (!this.observer) {
const options = {
root: null,
rootMargin: `0px 0px ${this.distance}px 0px`,
threshold: 1.0,
};
this.observer = new IntersectionObserver(
this.handleIntersection,
options,
);
this.observer.observe(this.$refs.sentinel as HTMLElement);
}
}
// 'beforeUnmount' hook runs before unmounting the component
beforeUnmount() {
if (this.observer) {
this.observer.disconnect();
}
}
@Emit("reached-bottom")
handleIntersection(entries: IntersectionObserverEntry[]) {
const entry = entries[0];
if (entry.isIntersecting) {
return true;
}
return false;
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

View File

@@ -0,0 +1,93 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Home',
'text-slate-500': selected !== 'Home',
}"
>
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Discover',
'text-slate-500': selected !== 'Discover',
}"
>
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Projects',
'text-slate-500': selected !== 'Projects',
}"
>
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Contacts',
'text-slate-500': selected !== 'Contacts',
}"
>
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Profile',
'text-slate-500': selected !== 'Profile',
}"
>
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
@Component
export default class QuickNav extends Vue {
@Prop selected = "";
}
</script>

View File

@@ -0,0 +1,110 @@
// from https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80
import * as TWEEN from "@tweenjs/tween.js";
import * as THREE from "three";
import { createCamera } from "./components/camera.js";
import { createLights } from "./components/lights.js";
import { createScene } from "./components/scene.js";
import { loadLandmarks } from "./components/objects/landmarks.js";
import { createTerrain } from "./components/objects/terrain.js";
import { Loop } from "./systems/Loop.js";
import { Resizer } from "./systems/Resizer.js";
import { createControls } from "./systems/controls.js";
import { createRenderer } from "./systems/renderer.js";
const COLOR1 = "#dddddd";
const COLOR2 = "#0055aa";
class World {
constructor(container, vue) {
this.PLATFORM_BORDER = 5;
this.PLATFORM_EDGE_FOR_UNKNOWNS = 10;
this.PLATFORM_SIZE = 100; // note that the loadLandmarks calculations may still assume 100
this.update = this.update.bind(this);
this.vue = vue;
// Instances of camera, scene, and renderer
this.camera = createCamera();
this.scene = createScene(COLOR2);
this.renderer = createRenderer();
// necessary for models, says https://threejs.org/docs/index.html#examples/en/loaders/GLTFLoader
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.light = null;
this.lights = [];
this.bushes = [];
// Initialize Loop
this.loop = new Loop(this.camera, this.scene, this.renderer);
container.append(this.renderer.domElement);
// Orbit Controls
const controls = createControls(this.camera, this.renderer.domElement);
// Light Instance, with optional light helper
const { light } = createLights(COLOR1);
// Terrain Instance
const terrain = createTerrain({
color: COLOR1,
height: this.PLATFORM_SIZE + this.PLATFORM_BORDER * 2,
width:
this.PLATFORM_SIZE +
this.PLATFORM_BORDER * 2 +
this.PLATFORM_EDGE_FOR_UNKNOWNS * 2,
});
this.loop.updatables.push(controls);
this.loop.updatables.push(light);
this.loop.updatables.push(terrain);
this.scene.add(light, terrain);
loadLandmarks(vue, this, this.scene, this.loop);
requestAnimationFrame(this.update);
// Responsive handler
const resizer = new Resizer(container, this.camera, this.renderer);
resizer.onResize = () => {
this.render();
};
}
update(time) {
TWEEN.update(time);
this.lights.forEach((light) => {
light.updateMatrixWorld();
light.target.updateMatrixWorld();
});
this.lights.forEach((bush) => {
bush.updateMatrixWorld();
});
requestAnimationFrame(this.update);
}
render() {
// draw a single frame
this.renderer.render(this.scene, this.camera);
}
// Animation handlers
start() {
this.loop.start();
}
stop() {
this.loop.stop();
}
setExposedWorldProperties(key, value) {
this.vue.setWorldProperty(key, value);
}
}
export { World };

View File

@@ -0,0 +1,19 @@
import { PerspectiveCamera } from "three";
function createCamera() {
const camera = new PerspectiveCamera(
35, // fov = Field Of View
1, // aspect ratio (dummy value)
0.1, // near clipping plane
350, // far clipping plane
);
// move the camera back so we can view the scene
camera.position.set(0, 100, 200);
// eslint-disable-next-line @typescript-eslint/no-empty-function
camera.tick = () => {};
return camera;
}
export { createCamera };

View File

@@ -0,0 +1,14 @@
import { DirectionalLight, DirectionalLightHelper } from "three";
function createLights(color) {
const light = new DirectionalLight(color, 4);
const lightHelper = new DirectionalLightHelper(light, 0);
light.position.set(60, 100, 30);
// eslint-disable-next-line @typescript-eslint/no-empty-function
light.tick = () => {};
return { light, lightHelper };
}
export { createLights };

View File

@@ -0,0 +1,254 @@
import axios from "axios";
import * as R from "ramda";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
const ANIMATION_DURATION_SECS = 10;
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
export async function loadLandmarks(vue, world, scene, loop) {
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer;
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const headers = {
"Content-Type": "application/json",
};
const identity = JSON.parse(account?.identity || "null");
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const resp = await axios.get(url, { headers: headers });
if (resp.status === 200) {
const landmarks = resp.data.data;
const minDate = landmarks[landmarks.length - 1].issuedAt;
const maxDate = landmarks[0].issuedAt;
world.setExposedWorldProperties("startTime", minDate.replace("T", " "));
world.setExposedWorldProperties("endTime", maxDate.replace("T", " "));
const minTimeMillis = new Date(minDate).getTime();
const fullTimeMillis =
maxDate > minDate ? new Date(maxDate).getTime() - minTimeMillis : 1; // avoid divide by zero
// ratio of animation time to real time
const fakeRealRatio = (ANIMATION_DURATION_SECS * 1000) / fullTimeMillis;
// load plant model first because it takes a second
const loader = new GLTFLoader();
// choose the right plant
const modelLoc = "/models/lupine_plant/scene.gltf", // push with pokies
modScale = 0.1;
//const modelLoc = "/models/round_bush/scene.gltf", // green & pink
// modScale = 1;
//const modelLoc = "/models/coreopsis-flower.glb", // 3 flowers
// modScale = 2;
//const modelLoc = "/models/a_bush/scene.gltf", // purple leaves
// modScale = 15;
// calculate positions for each claim, especially because some are random
const locations = landmarks.map((claim) =>
locForGive(
claim,
world.PLATFORM_SIZE,
world.PLATFORM_EDGE_FOR_UNKNOWNS,
),
);
// eslint-disable-next-line @typescript-eslint/no-this-alias
loader.load(
modelLoc,
function (gltf) {
gltf.scene.scale.set(0, 0, 0);
for (let i = 0; i < landmarks.length; i++) {
// claim is a GiveServerRecord (see endorserServer.ts)
const claim = landmarks[i];
const newPlant = SkeletonUtils.clone(gltf.scene);
const loc = locations[i];
newPlant.position.set(loc.x, 0, loc.z);
world.scene.add(newPlant);
const timeDelayMillis =
fakeRealRatio *
(new Date(claim.issuedAt).getTime() - minTimeMillis);
new TWEEN.Tween(newPlant.scale)
.delay(timeDelayMillis)
.to({ x: modScale, y: modScale, z: modScale }, 5000)
.start();
world.bushes = [...world.bushes, newPlant];
}
},
undefined,
function (error) {
console.error(error);
},
);
// calculate when lights shine on appearing claim area
for (let i = 0; i < landmarks.length; i++) {
// claim is a GiveServerRecord (see endorserServer.ts)
const claim = landmarks[i];
const loc = locations[i];
const light = createLight();
light.position.set(loc.x, 20, loc.z);
light.target.position.set(loc.x, 0, loc.z);
loop.updatables.push(light);
scene.add(light);
scene.add(light.target);
// now figure out the timing and shine a light
const timeDelayMillis =
fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis);
new TWEEN.Tween(light)
.delay(timeDelayMillis)
.to({ intensity: 100 }, 10)
.chain(
new TWEEN.Tween(light.position)
.to({ y: 5 }, 5000)
.onComplete(() => {
scene.remove(light);
light.dispose();
}),
)
.start();
world.lights = [...world.lights, light];
}
} else {
console.error(
"Got bad server response status & data of",
resp.status,
resp.data,
);
vue.setAlert(
"Error With Server",
"There was an error retrieving your claims from the server.",
);
}
} catch (error) {
console.error("Got exception contacting server:", error);
vue.setAlert(
"Error With Server",
"There was a problem retrieving your claims from the server.",
);
}
}
/**
*
* @param giveClaim
* @returns {x:float, z:float} where -50 <= x & z < 50
*/
function locForGive(giveClaim, platformWidth, borderWidth) {
let loc;
if (giveClaim?.claim?.recipient?.identifier) {
// this is directly to a person
loc = locForEthrDid(giveClaim.claim.recipient.identifier);
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
} else if (giveClaim?.object?.isPartOf?.identifier) {
// this is probably to a project
const objId = giveClaim.object.isPartOf.identifier;
if (objId.startsWith(ENDORSER_ENTITY_PREFIX)) {
loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length));
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
}
}
if (!loc) {
// it must be outside our known addresses so let's put it somewhere random on the side
const leftSide = Math.random() < 0.5;
loc = {
x: leftSide
? -platformWidth / 2 - borderWidth / 2
: platformWidth / 2 + borderWidth / 2,
z: Math.random() * platformWidth - platformWidth / 2,
};
}
return loc;
}
/**
* Generate a deterministic x & z location based on the randomness of an ID.
*
* We'd like the location to fully map back to the original ID.
* This typically means we use half the ID for the x and half for the z.
*
* ... in this case: a ULID.
* We'll use the first half (13 characters) for the x coordinate and next 13 for the z.
* We recognize that this is only 3 characters = 15 bits = 32768 unique values
* for the random part for the first half. We also recognize that those random
* bits may be shared with previous ULIDs if they were generated in the same
* millisecond, and therefore much of the evenness of the distribution depends
* on the other dimension.
*
* Also: since the first 10 characters are time-based, we're going to reverse
* the order of the characters to make the randomness more evenly distributed.
* This is reversing the order of the 5-bit characters, not each of the bits.
* Also wik: the first characters of the second half might be the same as
* previous ULIDs if they were generated in the same millisecond. So it's
* best to have that last character be the most significant bit so that there
* is a more even distribution in that dimension.
*
* @param ulid
* @returns {x: float, z: float} where 0 <= x & z < 100
*/
function locForUlid(ulid) {
const xChars = ulid.substring(0, 13).split("").reverse().join("");
const zChars = ulid.substring(13, 26).split("").reverse().join("");
// from https://github.com/ulid/javascript/blob/5e9727b527aec5b841737c395a20085c4361e971/lib/index.ts#L21
const BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford's Base32
// We're currently only using 1024 possible x and z values
// because the display is pretty low-fidelity at this point.
const rawX = BASE32.indexOf(xChars[1]) * 32 + BASE32.indexOf(xChars[0]);
const rawZ = BASE32.indexOf(zChars[1]) * 32 + BASE32.indexOf(zChars[0]);
const x = (100 * rawX) / 1024;
const z = (100 * rawZ) / 1024;
return { x, z };
}
/**
* See locForUlid. Similar, but for ethr DIDs.
* @param did
* @returns {x: float, z: float} where 0 <= x & z < 100
*/
function locForEthrDid(did) {
// "did:ethr:0x..."
if (did.length < 51) {
return { x: 0, z: 0 };
} else {
const randomness = did.substring("did:ethr:0x".length);
// We'll use all the randomness for fully unique x & z values.
// But we'll only calculate this view with the first byte since our rendering resolution is low.
const xOff = parseInt(Number("0x" + randomness.substring(0, 2)), 10);
const x = (xOff * 100) / 256;
// ... and since we're reserving 20 bytes total for x, start z with character 20,
// again with one byte.
const zOff = parseInt(Number("0x" + randomness.substring(20, 22)), 10);
const z = (zOff * 100) / 256;
return { x, z };
}
}
function createLight() {
const light = new THREE.SpotLight(0xffffff, 0, 0, Math.PI / 8, 0.5, 0);
// eslint-disable-next-line @typescript-eslint/no-empty-function
light.tick = () => {};
return light;
}

View File

@@ -0,0 +1,29 @@
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
export function createTerrain(props) {
const loader = new TextureLoader();
const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg");
// w h
const geometry = new PlaneGeometry(props.width, props.height, 64, 64);
const material = new MeshLambertMaterial({
color: props.color,
flatShading: true,
map: height,
//displacementMap: height,
//displacementScale: 5,
});
const plane = new Mesh(geometry, material);
plane.position.set(0, 0, 0);
plane.rotation.x -= Math.PI * 0.5;
//Storing our original vertices position on a new attribute
plane.geometry.attributes.position.originalPosition =
plane.geometry.attributes.position.array;
// eslint-disable-next-line @typescript-eslint/no-empty-function
plane.tick = () => {};
return plane;
}

View File

@@ -0,0 +1,11 @@
import { Color, Scene } from "three";
function createScene(color) {
const scene = new Scene();
scene.background = new Color(color);
//scene.fog = new Fog(color, 60, 90);
return scene;
}
export { createScene };

View File

@@ -0,0 +1,33 @@
import { Clock } from "three";
const clock = new Clock();
class Loop {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
this.updatables = [];
}
start() {
this.renderer.setAnimationLoop(() => {
this.tick();
// render a frame
this.renderer.render(this.scene, this.camera);
});
}
stop() {
this.renderer.setAnimationLoop(null);
}
tick() {
const delta = clock.getDelta();
for (const object of this.updatables) {
object.tick(delta);
}
}
}
export { Loop };

View File

@@ -0,0 +1,33 @@
const setSize = (container, camera, renderer) => {
// These are great for full-screen, which adjusts to a window.
const height = window.innerHeight;
const width = window.innerWidth - 50;
// These are better for fitting in a container, which stays that size.
//const height = container.scrollHeight;
//const width = container.scrollWidth;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
};
class Resizer {
constructor(container, camera, renderer) {
// set initial size on load
setSize(container, camera, renderer);
window.addEventListener("resize", () => {
// set the size again if a resize occurs
setSize(container, camera, renderer);
// perform any custom actions
this.onResize();
});
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onResize() {}
}
export { Resizer };

View File

@@ -0,0 +1,38 @@
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { MathUtils } from "three";
function createControls(camera, canvas) {
const controls = new OrbitControls(camera, canvas);
//enable controls?
controls.enabled = true;
controls.autoRotate = false;
//controls.autoRotateSpeed = 0.2;
// control limits
// It's recommended to set some control boundaries,
// to prevent the user from clipping with the objects.
// y axis
controls.minPolarAngle = MathUtils.degToRad(40); // default
controls.maxPolarAngle = MathUtils.degToRad(75);
// x axis
// controls.minAzimuthAngle = ...
// controls.maxAzimuthAngle = ...
//smooth camera:
// remember to add to loop updatables to work
controls.enableDamping = true;
//controls.enableZoom = false;
controls.maxDistance = 250;
//controls.enablePan = false;
controls.tick = () => controls.update();
return controls;
}
export { createControls };

View File

@@ -0,0 +1,13 @@
import { WebGLRenderer } from "three";
function createRenderer() {
const renderer = new WebGLRenderer({ antialias: true });
// turn on the physically correct lighting model
// (The browser complains: "THREE.WebGLRenderer: the property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead." However, that changes the lighting in a way that doesn't look better.)
renderer.physicallyCorrectLights = true;
return renderer;
}
export { createRenderer };

View File

@@ -2,8 +2,11 @@
* Generic strings that could be used throughout the app.
*/
export enum AppString {
APP_NAME = "KickStart with Time",
VERSION = "0.1",
DEFAULT_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
//DEFAULT_ENDORSER_API_SERVER = "http://localhost:3000",
APP_NAME = "Kick-Start with Time",
PROD_ENDORSER_API_SERVER = "https://endorser.ch:3000",
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
}

View File

@@ -7,6 +7,7 @@ import {
Settings,
SettingsSchema,
} from "./tables/settings";
import { AppString } from "@/constants/app";
// a separate DB because the seed is super-sensitive data
type SensitiveTables = {
@@ -40,7 +41,12 @@ const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
*/
// create password and place password in localStorage
/**
* Create password and place password in localStorage.
*
* It's good practice to keep the data encrypted at rest, so we'll do that even
* if the secret is stored right next to the app.
*/
const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
@@ -48,7 +54,6 @@ if (localStorage.getItem("secret") == null) {
localStorage.setItem("secret", secret);
}
//console.log("IndexedDB Encryption Secret:", secret);
encrypted(accountsDB, { secretKey: secret });
accountsDB.version(1).stores(SensitiveSchemas);
@@ -57,5 +62,8 @@ db.version(1).stores(NonsensitiveSchemas);
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
db.on("populate", function () {
// ensure there's an initial entry for settings
db.settings.add({ id: MASTER_SETTINGS_KEY });
db.settings.add({
id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
});
});

View File

@@ -3,6 +3,8 @@ export type Account = {
dateCreated: string;
derivationPath: string;
did: string;
// stringified JSON containing underlying key material of type IIdentifier
// https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
identity: string;
publicKeyHex: string;
mnemonic: string;

View File

@@ -1,9 +1,12 @@
// a singleton
export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
lastViewedClaimId?: string;
showContactGivesInline?: boolean;
};

View File

@@ -20,7 +20,7 @@ export const newIdentifier = (
address: string,
publicHex: string,
privateHex: string,
derivationPath: string
derivationPath: string,
): Omit<IIdentifier, keyof "provider"> => {
return {
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
@@ -46,7 +46,7 @@ export const newIdentifier = (
* @return {*} {[string, string, string, string]}
*/
export const deriveAddress = (
mnemonic: string
mnemonic: string,
): [string, string, string, string] => {
const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
mnemonic = mnemonic.trim().toLowerCase();
@@ -134,7 +134,7 @@ export function fromJose(signature: string): {
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
);
}
const r = bytesToHex(signatureBytes.slice(0, 32));

View File

@@ -1,3 +1,10 @@
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts";
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
export const SERVICE_ID = "endorser.ch";
@@ -9,6 +16,25 @@ export interface AgreeVerifiableCredential {
object: Record<any, any>;
}
export interface GiverInputInfo {
did?: string;
name?: string;
}
export interface ClaimResult {
success: { claimId: string; handleId: string };
error: { code: string; message: string };
}
export interface GenericClaim {
"@context": string;
"@type": string;
issuedAt: string;
// "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>;
}
export interface GiveServerRecord {
agentDid: string;
amount: number;
@@ -24,10 +50,11 @@ export interface GiveServerRecord {
export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": string;
agent: { identifier: string };
agent?: { identifier: string };
description?: string;
fulfills?: { "@type": string; identifier: string };
identifier?: string;
object: { amountOfThisGood: number; unitCode: string };
object?: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string };
}
@@ -38,3 +65,182 @@ export interface RegisterVerifiableCredential {
object: string;
recipient: { identifier: string };
}
export interface InternalError {
error: string; // for system logging
userMessage?: string; // for user display
}
// 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";
export function isHiddenDid(did) {
return did === HIDDEN_DID;
}
/**
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
**/
export function didInfo(did, activeDid, allMyDids, contacts) {
const myId: string | undefined = R.find(R.identity, allMyDids);
if (myId) {
return "You" + (myId.did !== activeDid ? " (Alt ID)" : "");
} else {
const contact: Contact | undefined = R.find((c) => c.did === did, contacts);
if (contact) {
return contact.name || "Someone Unnamed in Contacts";
} else if (!did) {
return "Unspecified Person";
} else if (isHiddenDid(did)) {
return "Someone Not In Network";
} else {
return "Someone Not In Contacts";
}
}
}
/**
* For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim
*
* @param identity
* @param fromDid may be null
* @param toDid
* @param description may be null; should have this or hours
* @param hours may be null; should have this or description
*/
export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
identity: IIdentifier,
fromDid: string,
toDid: string,
description: string,
hours: number,
fulfillsProjectHandleId?: string,
): Promise<AxiosResponse<ClaimResult> | InternalError> {
// Make a claim
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
recipient: { identifier: toDid },
};
if (fromDid) {
vcClaim.agent = { identifier: fromDid };
}
if (description) {
vcClaim.description = description;
}
if (hours) {
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
}
if (fulfillsProjectHandleId) {
vcClaim.fulfills = {
"@type": "PlanAction",
identifier: fulfillsProjectHandleId,
};
}
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) {
return new Promise<InternalError>((resolve, reject) => {
reject({
error: "No private key",
message:
"Your identifier " +
identity.did +
" is not configured correctly. Use a different identifier.",
});
});
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return axios.post(url, payload, { headers });
}
// from https://stackoverflow.com/a/175787/845494
//
export function isNumeric(str: string): boolean {
return !isNaN(+str);
}
export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}
export interface ErrorResponse {
error?: {
message?: string;
};
}
export interface RateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
/**
* Represents data about a project
**/
export interface ProjectData {
/**
* Name of the project
**/
name: string;
/**
* Description of the project
**/
description: string;
/**
* URL referencing information about the project
**/
handleId: string;
/**
* The Identier of the project
**/
rowid: string;
}
export interface VerifiableCredential {
"@context": string;
"@type": string;
name: string;
description: string;
identifier?: string;
}
export interface WorldProperties {
startTime?: string;
endTime?: string;
}

View File

@@ -1,100 +0,0 @@
/* import * as R from "ramda";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { IIdentifier } from "@veramo/core";
import { Contact } from "../entity/contact";
import { Settings } from "../entity/settings";
import * as utility from "../utility/utility";
const MAX_LOG_LENGTH = 2000000;
export const DEFAULT_ENDORSER_API_SERVER = "https://endorser.ch:3000";
export const DEFAULT_ENDORSER_VIEW_SERVER = "https://endorser.ch";
export const LOCAL_ENDORSER_API_SERVER = "http://127.0.0.1:3000";
export const LOCAL_ENDORSER_VIEW_SERVER = "http://127.0.0.1:3001";
export const TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000";
export const TEST_ENDORSER_VIEW_SERVER = "https://test.endorser.ch:8080";
// for contents set in reducers
interface Payload<T> {
type: string;
payload: T;
}
interface LogMsg {
log: boolean;
msg: string;
}
export const appSlice = createSlice({
name: "app",
initialState: {
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
settings: null as Settings,
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
identifiers: null as Array<IIdentifier> | null,
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
contacts: null as Array<Contact> | null,
viewServer: DEFAULT_ENDORSER_VIEW_SERVER,
logMessage: "",
advancedMode: false,
testMode: false,
},
reducers: {
addIdentifier: (state, contents: Payload<IIdentifier>) => {
state.identifiers = state.identifiers.concat([contents.payload]);
},
addLog: (state, contents: Payload<LogMsg>) => {
if (state.logMessage.length > MAX_LOG_LENGTH) {
state.logMessage =
"<truncated>\n..." +
state.logMessage.substring(
state.logMessage.length - MAX_LOG_LENGTH / 2
);
}
if (contents.payload.log) {
console.log(contents.payload.msg);
state.logMessage += "\n" + contents.payload.msg;
}
},
setAdvancedMode: (state, contents: Payload<boolean>) => {
state.advancedMode = contents.payload;
},
setContacts: (state, contents: Payload<Array<Contact>>) => {
state.contacts = contents.payload;
},
setContact: (state, contents: Payload<Contact>) => {
const index = R.findIndex(
(c) => c.did === contents.payload.did,
state.contacts
);
state.contacts[index] = contents.payload;
},
setHomeScreen: (state, contents: Payload<string>) => {
state.settings.homeScreen = contents.payload;
},
setIdentifiers: (state, contents: Payload<Array<IIdentifier>>) => {
state.identifiers = contents.payload;
},
setSettings: (state, contents: Payload<Settings>) => {
state.settings = contents.payload;
},
setTestMode: (state, contents: Payload<boolean>) => {
state.testMode = contents.payload;
},
setViewServer: (state, contents: Payload<string>) => {
state.viewServer = contents.payload;
},
},
});
export const appStore = configureStore({ reducer: appSlice.reducer });
*/

View File

@@ -81,7 +81,7 @@ function didProviderName(netName: string) {
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
DEFAULT_DID_PROVIDER_NETWORK_NAME
DEFAULT_DID_PROVIDER_NETWORK_NAME,
);
export const HANDY_APP = false;

View File

@@ -10,18 +10,25 @@ import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faBurst,
faCalendar,
faChevronLeft,
faChevronRight,
faCircle,
faCircleCheck,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFloppyDisk,
faFolderOpen,
faGift,
faHand,
faHouseChimney,
faLongArrowAltLeft,
@@ -35,6 +42,8 @@ import {
faRotate,
faShareNodes,
faSpinner,
faSquareCaretDown,
faSquareCaretUp,
faTrashCan,
faUser,
faUsers,
@@ -42,18 +51,25 @@ import {
} from "@fortawesome/free-solid-svg-icons";
library.add(
faBurst,
faCalendar,
faChevronLeft,
faChevronRight,
faCircle,
faCircleCheck,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFloppyDisk,
faFolderOpen,
faGift,
faHand,
faHouseChimney,
faLongArrowAltLeft,
@@ -67,10 +83,12 @@ library.add(
faRotate,
faShareNodes,
faSpinner,
faSquareCaretDown,
faSquareCaretUp,
faTrashCan,
faUser,
faUsers,
faXmark
faXmark,
);
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";

View File

@@ -7,7 +7,7 @@ if (process.env.NODE_ENV === "production") {
ready() {
console.log(
"App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB"
"For more details, visit https://goo.gl/AFskqB",
);
},
registered() {
@@ -24,7 +24,7 @@ if (process.env.NODE_ENV === "production") {
},
offline() {
console.log(
"No internet connection found. App is running in offline mode."
"No internet connection found. App is running in offline mode.",
);
},
error(error) {

View File

@@ -1,33 +1,36 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db";
/**
*
* @param to :RouteLocationNormalized
* @param from :RouteLocationNormalized
* @param next :NavigationGuardNext
*/
const enterOrStart = async (to, from, next) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
};
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: () =>
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"),
beforeEnter: async (to, from, next) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
},
},
{
path: "/about",
name: "about",
component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/account",
name: "account",
component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/confirm-contact",
@@ -45,11 +48,20 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
),
},
{
path: "/contact-qr",
name: "contact-qr",
component: () =>
import(
/* webpackChunkName: "contact-qr" */ "../views/ContactQRScanShowView.vue"
),
},
{
path: "/contacts",
name: "contacts",
component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/scan-contact",
@@ -103,6 +115,22 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
),
},
{
path: "/new-identifier",
name: "new-identifier",
component: () =>
import(
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
),
},
{
path: "/identity-switcher",
name: "identity-switcher",
component: () =>
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
},
{
path: "/project",
name: "project",
@@ -114,6 +142,15 @@ const routes: Array<RouteRecordRaw> = [
name: "projects",
component: () =>
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/seed-backup",
name: "seed-backup",
component: () =>
import(
/* webpackChunkName: "seed-backup" */ "../views/SeedBackupView.vue"
),
},
{
path: "/start",
@@ -121,6 +158,22 @@ const routes: Array<RouteRecordRaw> = [
component: () =>
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
},
{
path: "/statistics",
name: "statistics",
component: () =>
import(
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
),
},
{
path: "/contact-gives",
name: "contact-gives",
component: () =>
import(
/* webpackChunkName: "statistics" */ "../views/ContactGiftingView.vue"
),
},
];
/** @type {*} */

View File

@@ -49,7 +49,8 @@ export async function testServerRegisterUser() {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const endorserApiServer =
settings?.apiServer || AppString.TEST_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/claim";
const headers = {
"Content-Type": "application/json",

View File

@@ -1,14 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
})
export default class AboutView extends Vue {}
</script>

View File

@@ -1,59 +1,25 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
Your Identity
</h1>
<div class="flex justify-between">
<span />
<span class="whitespace-nowrap">
<router-link
:to="{ name: 'contact-qr' }"
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw"></fa>
</router-link>
</span>
<span />
</div>
<div class="flex justify-between py-2">
<span />
<span>
@@ -66,20 +32,22 @@
</span>
</div>
<!-- Friend referral requirement notice -->
<!-- Registration notice -->
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
<div
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Important:</b> before you can create a new project or commit time to
one, you need a friend to register you.
<b>Note:</b> Before you can publicly announce a new project or time
commitment, a friend needs to register you.
</p>
<button
id="btnShowQR"
<router-link
:to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
>
Share Your ID
</button>
Share Your Info
</router-link>
</div>
<!-- Identity Details -->
@@ -89,63 +57,82 @@
<div class="text-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ activeDid }}</code>
<button @click="copy(activeDid)" class="ml-2">
<button
@click="
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span class="whitespace-nowrap ml-4">
<button
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
>
<fa icon="share-nodes" class="fa-fw"></fa>
</button>
<button
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md ml-1"
>
<fa icon="qrcode" class="fa-fw"></fa>
</button>
</span>
<span v-show="showDidCopy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ publicBase64 }}</code>
<button @click="copy(publicBase64)" class="ml-2">
<button
@click="
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showB64Copy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ publicHex }}</code>
<button @click="copy(publicHex)" class="ml-2">
<button
@click="
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showPubCopy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ derivationPath }}</code>
<button @click="copy(derivationPath)" class="ml-2">
<button
@click="
doCopyTwoSecRedo(derivationPath, () => (showDerCopy = !showDerCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDerCopy">Copied!</span>
</div>
</div>
<router-link
:to="{ name: 'new-edit-account' }"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
>
Edit Identity
</router-link>
<router-link
:to="{ name: 'identity-switcher' }"
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
>
Switch Identity / No Identity
</router-link>
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
<a
<router-link
:to="{ name: 'seed-backup' }"
href=""
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Backup Identifier Seed
</a>
</router-link>
<a
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="exportDatabase()"
@@ -156,129 +143,163 @@
<!-- QR code popup -->
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
<form method="dialog">
<div class="text-slate-500 text-center">
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
</div>
<img src="img/sample-qr-code.png" class="w-full mb-3" />
<div class="text-slate-500 text-center">
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
</div>
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
<button
value="cancel"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Copy to Clipboard
</button>
<button
value="cancel"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Close
</button>
</form>
<button
value="cancel"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Copy to Clipboard
</button>
<button
value="cancel"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Close
</button>
</dialog>
<h3 class="text-sm uppercase font-semibold mb-3">Advanced</h3>
<label
for="toggleShowAmounts"
class="flex items-center cursor-pointer mb-6"
@click="handleChange"
<h3
class="text-sm uppercase font-semibold mb-3"
@click="showAdvanced = !showAdvanced"
>
<!-- toggle -->
<div class="relative">
<!-- input -->
<input
type="checkbox"
v-model="showContactGives"
name="showContactGives"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
<!-- label -->
<div class="ml-2">Show amounts given with contacts</div>
</label>
<div class="flex py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits
</button>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<div v-if="numAccounts > 0" class="flex py-2">
Switch Account
<span v-for="accountNum in numAccounts" :key="accountNum">
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
#{{ accountNum }}
</button>
</span>
</div>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
Advanced
</h3>
<div v-if="showAdvanced">
<label
for="toggleShowAmounts"
class="flex items-center cursor-pointer mb-6"
@click="handleChange"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
<!-- toggle -->
<div class="relative">
<!-- input -->
<input
type="checkbox"
v-model="showContactGives"
name="showContactGives"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
<!-- label -->
<div class="ml-2">Show amounts given with contacts</div>
</label>
<div class="flex py-2">
<button
class="text-center text-md text-blue-500"
@click="checkLimits()"
>
Check Limits
</button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<div class="flex py-2">
Claim Server
<input
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="apiServerInput"
/>
<button
v-if="apiServerInput != apiServer"
class="px-4 rounded bg-red-500 border border-slate-400"
@click="onClickSaveApiServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)"
>
Use Prod
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)"
>
Use Test
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)"
>
Use Local
</button>
</div>
<div>
<button class="text-blue-500">
<router-link
:to="{ name: 'statistics' }"
class="block text-center py-3"
>
See Achievements & Statistics
</router-link>
</button>
</div>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import "dexie-export-import";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
accessToken,
deriveAddress,
generateSeed,
newIdentifier,
} from "@/libs/crypto";
import { accessToken } from "@/libs/crypto";
import { AxiosError } from "axios/index";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import { IIdentifier } from "@veramo/core";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface RateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
@Component
@Component({ components: { AlertMessage, QuickNav } })
export default class AccountViewView extends Vue {
Constants = AppString;
activeDid = "";
apiServer = "";
apiServerInput = "";
derivationPath = "";
firstName = "";
lastName = "";
@@ -286,9 +307,45 @@ export default class AccountViewView extends Vue {
publicHex = "";
publicBase64 = "";
limits: RateLimits | null = null;
limitsMessage = "";
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
showContactGives = false;
copy = useClipboard().copy;
showDidCopy = false;
showDerCopy = false;
showB64Copy = false;
showPubCopy = false;
showAdvanced = false;
alertMessage = "";
alertTitle = "";
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text, fn) {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
}
handleChange() {
this.showContactGives = !this.showContactGives;
@@ -299,9 +356,13 @@ export default class AccountViewView extends Vue {
return timeStr.substring(0, timeStr.indexOf("T"));
}
// 'created' hook runs when the Vue instance is first created
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
async created() {
// Uncomment to register this user on the test server.
// Uncomment this to register this user on the test server.
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
@@ -311,52 +372,42 @@ export default class AccountViewView extends Vue {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.lastName = settings?.lastName || "";
this.showContactGives = !!settings?.showContactGivesInline;
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
if (this.numAccounts === 0) {
let address = ""; // 0x... ETH address, without "did:eth:"
let privateHex = "";
const mnemonic = generateSeed();
[address, privateHex, this.publicHex, this.derivationPath] =
deriveAddress(mnemonic);
const identity = await this.getIdentity(this.activeDid);
const newId = newIdentifier(
address,
this.publicHex,
privateHex,
this.derivationPath
if (identity) {
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString(
"base64",
);
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: this.derivationPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
this.derivationPath = identity.keys[0].meta.derivationPath;
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
this.activeDid = newId.did;
this.checkLimitsFor(identity);
}
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath;
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.log("Telling user to clear cache because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
if (
err.message ===
"Attempted to load account records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.alertMessage =
"Clear your cache and start over (after data backup).";
console.error(
"Telling user to clear cache at page create because:",
err,
);
this.alertTitle = "Error Creating Account";
}
}
}
@@ -368,10 +419,12 @@ export default class AccountViewView extends Vue {
});
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.log("Telling user to clear cache because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
"Clear your cache and start over (after data backup).";
console.error(
"Telling user to clear cache after contact setting update because:",
err,
);
this.alertTitle = "Error Updating Contact Setting";
}
}
@@ -389,65 +442,83 @@ export default class AccountViewView extends Vue {
this.alertTitle = "Download Started";
this.alertMessage = "See your downloads directory for the backup.";
this.isAlertVisible = true;
} catch (error) {
this.alertTitle = "Export Error";
this.alertMessage = "See console logs for more info.";
this.isAlertVisible = true;
console.error("Export Error:", error);
}
}
async checkLimits() {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/report/rateLimits";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.checkLimitsFor(identity);
}
}
async checkLimitsFor(identity: IIdentifier) {
this.loadingLimits = true;
this.limitsMessage = "";
try {
const url = this.apiServer + "/api/report/rateLimits";
const headers = await this.getHeaders(identity);
const resp = await this.axios.get(url, { headers });
// axios throws an exception on a 400
if (resp.status === 200) {
this.limits = resp.data;
}
} catch (error: unknown) {
const serverError = error as AxiosError;
this.alertTitle = "Error from Server";
console.log("Bad response retrieving limits: ", serverError);
// Anybody know how to access items inside "response.data" without this?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = serverError.response?.data;
if (data.error.message) {
this.alertMessage = data.error.message;
if (
error.message ===
"Attempted to load Give records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.alertMessage = "Bad server response. See logs for details.";
const serverError = error as AxiosError;
console.error("Bad response retrieving limits: ", serverError);
const data: ErrorResponse | undefined =
serverError.response && serverError.response.data;
if (data && data.error && data.error.message) {
this.limitsMessage = data.error.message;
} else {
this.limitsMessage = "Bad server response.";
}
}
this.isAlertVisible = true;
}
this.loadingLimits = false;
}
async switchAccount(accountNum: number) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = accounts[accountNum - 1];
// 0 means none
if (accountNum === 0) {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: undefined,
});
this.activeDid = "";
this.derivationPath = "";
this.publicHex = "";
this.publicBase64 = "";
} else {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = accounts[accountNum - 1];
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
this.activeDid = account.did;
this.derivationPath = account.derivationPath;
this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.activeDid = account.did;
this.derivationPath = account.derivationPath;
this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
}
}
public showContactGivesClassNames() {
@@ -457,31 +528,16 @@ export default class AccountViewView extends Vue {
};
}
alertMessage = "";
alertTitle = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
async onClickSaveApiServer() {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
apiServer: this.apiServerInput,
});
this.apiServer = this.apiServerInput;
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
setApiServerInput(value) {
this.apiServerInput = value;
}
}
</script>

View File

@@ -15,42 +15,40 @@
</h1>
</div>
<form>
<p class="text-center text-xl mb-4 font-light">
Would you like to add <i>Firstname</i> to your network?
</p>
<p class="text-center text-xl mb-4 font-light">
Would you like to add <i>Firstname</i> to your network?
</p>
<!-- Account Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-semibold mb-2">Firstname Lastname</h2>
<!-- Account Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-semibold mb-2">Firstname Lastname</h2>
<div class="text-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 mb-1">
<span><code>did:peer:kl45kj41lk451kl3</code></span>
</div>
<div class="text-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 mb-1">
<span><code>did:peer:kl45kj41lk451kl3</code></span>
</div>
</div>
<div class="mt-8">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Add Contact"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
</div>
</form>
<div class="mt-8">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Add Contact"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
</div>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
@Options({
@Component({
components: {},
})
export default class ConfirmContactView extends Vue {}

View File

@@ -1,48 +1,5 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<QuickNav selected="Contacts"></QuickNav>
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Given with {{ contact?.name }}
@@ -109,28 +66,20 @@
</div>
</div>
</div>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { AppString } from "@/constants/app";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import {
AgreeVerifiableCredential,
@@ -140,102 +89,125 @@ import {
} from "@/libs/endorserServer";
import * as didJwt from "did-jwt";
import { AxiosError } from "axios";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Options({})
@Component({ components: { AlertMessage, QuickNav } })
export default class ContactsView extends Vue {
activeDid = "";
apiServer = "";
contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = [];
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const contactDid = this.$route.query.contactDid as string;
this.contact = (await db.contacts.get(contactDid)) || null;
try {
await db.open();
const contactDid = this.$route.query.contactDid as string;
this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact);
if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact);
}
} catch (err) {
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
}
}
async loadGives(activeDid: string, contact: Contact) {
// only load the private keys temporarily when needed
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// load all the time I have given to them
try {
const identity = await this.getIdentity(this.activeDid);
let result = [];
const url =
endorserApiServer +
this.apiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did) +
"&recipientDid=" +
encodeURIComponent(contact.did);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const headers = this.getHeaders(identity);
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
result = resp.data.data;
} else {
console.log(
console.error(
"Got bad response status & data of",
resp.status,
resp.data
resp.data,
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
const url2 =
endorserApiServer +
this.apiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(contact.did) +
"&recipientDid=" +
encodeURIComponent(identity.did);
const token2 = await accessToken(identity);
const headers2 = {
"Content-Type": "application/json",
Authorization: "Bearer " + token2,
};
const headers2 = await this.getHeaders(identity);
const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) {
result = R.concat(result, resp2.data.data);
} else {
console.log(
console.error(
"Got bad response status & data of",
resp2.status,
resp2.data
resp2.data,
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
const sortedResult: Array<GiveServerRecord> = R.sort(
(a, b) =>
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
result
result,
);
this.giveRecords = sortedResult;
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
}
@@ -264,10 +236,7 @@ export default class ContactsView extends Vue {
};
// Create a signature using private key of identity
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = await this.getIdentity(this.activeDid);
if (identity.keys[0].privateKeyHex !== null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
@@ -282,8 +251,7 @@ export default class ContactsView extends Vue {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@@ -292,7 +260,6 @@ export default class ContactsView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success) {
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
}
@@ -311,7 +278,6 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see.
this.alertTitle = "Error With Server";
this.alertMessage = userMessage;
this.isAlertVisible = true;
}
}
}
@@ -319,34 +285,6 @@ export default class ContactsView extends Vue {
cannotConfirmMessage() {
this.alertTitle = "Not Allowed";
this.alertMessage = "Only the recipient can confirm final receipt.";
this.isAlertVisible = true;
}
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>

View File

@@ -0,0 +1,265 @@
<template>
<QuickNav selected="Home"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<router-link
:to="{ name: 'home' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
Give to Contacts
</h1>
</div>
<!-- Quick Search -->
<!-- Initial Loading Animation -->
<!-- Results List -->
<ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center">
<span class="grow italic"
><fa icon="question-circle" class="fa-fw fa-xl text-slate-400"></fa>
Anonymous
</span>
<span class="text-right">
<button
type="button"
@click="openDialog()"
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
>
<fa icon="gift" class="fa-fw"></fa>
</button>
</span>
</h2>
</li>
<li
v-for="contact in allContacts"
:key="contact.did"
class="border-b border-slate-300 py-3"
>
<h2 class="text-base flex gap-4 items-center">
<span class="grow font-semibold"
><fa icon="user" class="fa-fw fa-xl text-slate-400"></fa>
{{ contact.name || "(no name)" }}
</span>
<span class="text-right">
<button
type="button"
@click="openDialog(contact)"
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
>
<fa icon="gift" class="fa-fw"></fa>
</button>
</span>
</h2>
</li>
</ul>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({
components: { GiftedDialog, AlertMessage, QuickNav },
})
export default class HomeView extends Vue {
activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = [];
apiServer = "";
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
accounts: AccountsSchema;
numAccounts = 0;
async beforeCreate() {
accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() {
try {
await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray();
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed();
} catch (err) {
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
}
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
openDialog(giver) {
this.$refs.customDialog.open(giver);
}
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
});
} else {
// action was "cancel" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(giverDid, description, hours) {
if (!this.activeDid) {
this.setAlert(
"Error",
"You must select an identity before you can record a give.",
);
return;
}
if (!description && !hours) {
this.setAlert(
"Error",
"You must enter a description or some number of hours.",
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
);
if (isGiveCreationError(result)) {
const errorMessage = getGiveCreationErrorMessage(result);
console.log("Error with give result:", result);
this.setAlert(
"Error",
errorMessage || "There was an error recording the give.",
);
} else {
this.setAlert("Success", "That gift was recorded.");
}
} catch (error) {
console.log("Error with give caught:", error);
this.setAlert(
"Error",
getGiveErrorMessage(error) || "There was an error recording the give.",
);
}
}
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability
isGiveCreationError(result) {
return result.status !== 201 || result.data?.error;
}
getGiveCreationErrorMessage(result) {
return result.data?.error?.message;
}
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
}
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Contact Info
</h1>
<!--
Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
-->
<QRCodeVue3
:value="this.qrValue"
:cornersSquareOptions="{ type: 'extra-rounded' }"
:dotsOptions="{ type: 'square' }"
class="flex justify-center"
/>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as R from "ramda";
import { SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
alertTitle = "";
alertMessage = "";
@Component({
components: {
QRCodeVue3,
AlertMessage,
QuickNav,
},
})
export default class ContactQRScanShow extends Vue {
activeDid = "";
apiServer = "";
qrValue = "";
public async getIdentity(activeDid) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (!account) {
this.alertMessage = "You have no identity yet.";
} else {
const identity = await this.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const contactInfo = {
iat: Date.now(),
iss: this.activeDid,
own: {
name: (settings?.firstName || "") + " " + (settings?.lastName || ""),
publicEncKey,
},
};
const alg = undefined;
const privateKeyHex: string = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex);
// create a JWT for the request
const vcJwt: string = await didJwt.createJWT(contactInfo, {
alg: alg,
issuer: identity.did,
signer: signer,
});
const viewPrefix = "https://endorser.ch/contact?jwt=";
this.qrValue = viewPrefix + vcJwt;
}
}
}
</script>

View File

@@ -14,82 +14,76 @@
</h1>
</div>
<form>
<h3 class="text-sm uppercase font-semibold mb-2">Scan a QR Code</h3>
<div class="bg-black rounded overflow-hidden relative mb-4">
<img src="https://picsum.photos/400/400?random=1" class="w-full" />
<h3 class="text-sm uppercase font-semibold mb-2">Scan a QR Code</h3>
<div class="bg-black rounded overflow-hidden relative mb-4">
<img src="https://picsum.photos/400/400?random=1" class="w-full" />
<!-- Darken overlay -->
<!-- Top -->
<div class="absolute top-0 left-0 right-0 bg-black/50 h-1/4"></div>
<!-- Reft -->
<div class="absolute top-1/4 bottom-1/4 left-0 bg-black/50 w-1/4"></div>
<!-- Right -->
<div
class="absolute top-1/4 bottom-1/4 right-0 bg-black/50 w-1/4"
></div>
<!-- Bottom -->
<div class="absolute bottom-0 left-0 right-0 bg-black/50 h-1/4"></div>
<!-- Darken overlay -->
<!-- Top -->
<div class="absolute top-0 left-0 right-0 bg-black/50 h-1/4"></div>
<!-- Reft -->
<div class="absolute top-1/4 bottom-1/4 left-0 bg-black/50 w-1/4"></div>
<!-- Right -->
<div class="absolute top-1/4 bottom-1/4 right-0 bg-black/50 w-1/4"></div>
<!-- Bottom -->
<div class="absolute bottom-0 left-0 right-0 bg-black/50 h-1/4"></div>
<!-- Reticle overlay -->
<!-- Top-left -->
<div
class="absolute top-1/4 left-1/4 h-6 w-6 border-white border-t-4 border-l-4 drop-shadow"
></div>
<!-- Top-right -->
<div
class="absolute top-1/4 right-1/4 h-6 w-6 border-white border-t-4 border-r-4 drop-shadow"
></div>
<!-- Bottom-left -->
<div
class="absolute bottom-1/4 left-1/4 h-6 w-6 border-white border-b-4 border-l-4 drop-shadow"
></div>
<!-- Bottom-right -->
<div
class="absolute bottom-1/4 right-1/4 h-6 w-6 border-white border-b-4 border-r-4 drop-shadow"
></div>
</div>
<!-- Reticle overlay -->
<!-- Top-left -->
<div
class="absolute top-1/4 left-1/4 h-6 w-6 border-white border-t-4 border-l-4 drop-shadow"
></div>
<!-- Top-right -->
<div
class="absolute top-1/4 right-1/4 h-6 w-6 border-white border-t-4 border-r-4 drop-shadow"
></div>
<!-- Bottom-left -->
<div
class="absolute bottom-1/4 left-1/4 h-6 w-6 border-white border-b-4 border-l-4 drop-shadow"
></div>
<!-- Bottom-right -->
<div
class="absolute bottom-1/4 right-1/4 h-6 w-6 border-white border-b-4 border-r-4 drop-shadow"
></div>
</div>
<h3 class="text-sm uppercase font-semibold mb-2">
or Enter Contact Data
</h3>
<h3 class="text-sm uppercase font-semibold mb-2">or Enter Contact Data</h3>
<input
type="text"
placeholder="Name (optional)"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
/>
<input
type="text"
placeholder="ID"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
/>
<input
type="text"
placeholder="Public Key (optional)"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<div class="mt-8">
<input
type="text"
placeholder="Name (optional)"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Look Up Contact"
/>
<input
type="text"
placeholder="ID"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
/>
<input
type="text"
placeholder="Public Key (optional)"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
/>
<div class="mt-8">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Look Up Contact"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
</div>
</form>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Cancel
</button>
</div>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
@Options({
@Component({
components: {},
})
export default class ContactScanView extends Vue {}

View File

@@ -1,54 +1,23 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<QuickNav selected="Contacts"></QuickNav>
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Contacts
</h1>
<div class="flex justify-between py-2">
<span />
<span>
<router-link
:to="{ name: 'help' }"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
>
Help
</router-link>
</span>
</div>
<!-- New Contact -->
<div class="mb-4 flex">
<input
@@ -101,7 +70,7 @@
</div>
<!-- Results List -->
<ul class="">
<ul v-if="contacts.length > 0">
<li
class="border-b border-slate-300"
v-for="contact in contacts"
@@ -165,8 +134,11 @@
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span class="tooltiptext-left">
{{ givenByMeDescriptions[contact.did] || "Nothing" }}
<span
v-if="givenByMeDescriptions[contact.did]"
class="tooltiptext-left"
>
{{ givenByMeDescriptions[contact.did] }}
</span>
<button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@@ -187,8 +159,11 @@
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span class="tooltiptext-left">
{{ givenToMeDescriptions[contact.did] || "Nothing" }}
<span
v-if="givenToMeDescriptions[contact.did]"
class="tooltiptext-left"
>
{{ givenToMeDescriptions[contact.did] }}
</span>
<button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@@ -212,18 +187,12 @@
</div>
</li>
</ul>
<p v-else>This identity has no contacts.</p>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
@@ -231,28 +200,28 @@ import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import {
GiveServerRecord,
GiveVerifiableCredential,
RegisterVerifiableCredential,
SERVICE_ID,
} from "@/libs/endorserServer";
import { Component, Vue } from "vue-facing-decorator";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@Options({
components: {},
@Component({
components: { AlertMessage, QuickNav },
})
export default class ContactsView extends Vue {
activeDid = "";
apiServer = "";
contacts: Array<Contact> = [];
contactInput = "";
// { "did:...": concatenated-descriptions } entry for each contact
@@ -272,12 +241,14 @@ export default class ContactsView extends Vue {
showGiveNumbers = false;
showGiveTotals = true;
showGiveConfirmed = true;
alertTitle = "";
alertMessage = "";
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.showGiveNumbers = !!settings?.showContactGivesInline;
if (this.showGiveNumbers) {
@@ -286,132 +257,119 @@ export default class ContactsView extends Vue {
const allContacts = await db.contacts.toArray();
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
allContacts,
);
}
async loadGives() {
public async getIdentity(activeDid) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
console.error(
"Attempted to load Give records with no identity available."
throw new Error(
"Attempted to load Give records with no identity available.",
);
return;
}
return identity;
}
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
public async getHeadersAndIdentity(activeDid) {
const identity = await this.getIdentity(activeDid);
const headers = await this.getHeaders(identity);
return { headers, identity };
}
async loadGives() {
const handleResponse = (resp, descriptions, confirmed, unconfirmed) => {
if (resp.status === 200) {
const allData = resp.data.data;
for (const give of allData) {
if (give.unit === "HUR") {
if (give.amountConfirmed) {
const prevAmount = confirmed[give.agentDid] || 0;
confirmed[give.agentDid] = prevAmount + give.amount;
} else {
const prevAmount = unconfirmed[give.agentDid] || 0;
unconfirmed[give.agentDid] = prevAmount + give.amount;
}
if (!descriptions[give.agentDid] && give.description) {
descriptions[give.agentDid] = give.description;
}
}
}
} else {
console.error(
"Got bad response status & data of",
resp.status,
resp.data,
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your " +
resp.config.url.includes("recipientDid")
? "received"
: "given" + " time from the server.";
}
};
// load all the time I have given
try {
const url =
endorserApiServer +
const { headers, identity } = await this.getHeadersAndIdentity(
this.activeDid,
);
const givenByUrl =
this.apiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
console.log("All gifts you've given:", resp.data);
if (resp.status === 200) {
const contactDescriptions: Record<string, string> = {};
const contactConfirmed: Record<string, number> = {};
const contactUnconfirmed: Record<string, number> = {};
const allData: Array<GiveServerRecord> = resp.data.data;
for (const give of allData) {
if (give.unit == "HUR") {
const recipDid: string = give.recipientDid;
if (give.amountConfirmed) {
const prevAmount = contactConfirmed[recipDid] || 0;
contactConfirmed[recipDid] = prevAmount + give.amount;
} else {
const prevAmount = contactUnconfirmed[recipDid] || 0;
contactUnconfirmed[recipDid] = prevAmount + give.amount;
}
if (!contactDescriptions[recipDid] && give.description) {
// Since many make the tooltip too big, we'll just use the latest.
contactDescriptions[recipDid] = give.description;
}
}
}
//console.log("Done retrieving gives", contactConfirmed);
this.givenByMeDescriptions = contactDescriptions;
this.givenByMeConfirmed = contactConfirmed;
this.givenByMeUnconfirmed = contactUnconfirmed;
} else {
console.log(
"Got bad response status & data of",
resp.status,
resp.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
// load all the time I have received
try {
const url =
endorserApiServer +
const givenToUrl =
this.apiServer +
"/api/v2/report/gives?recipientDid=" +
encodeURIComponent(identity.did);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
console.log("All gifts you've recieved:", resp.data);
if (resp.status === 200) {
const contactDescriptions: Record<string, string> = {};
const contactConfirmed: Record<string, number> = {};
const contactUnconfirmed: Record<string, number> = {};
const allData: Array<GiveServerRecord> = resp.data.data;
for (const give of allData) {
if (give.unit == "HUR") {
if (give.amountConfirmed) {
const prevAmount = contactConfirmed[give.agentDid] || 0;
contactConfirmed[give.agentDid] = prevAmount + give.amount;
} else {
const prevAmount = contactUnconfirmed[give.agentDid] || 0;
contactUnconfirmed[give.agentDid] = prevAmount + give.amount;
}
if (!contactDescriptions[give.agentDid] && give.description) {
// Since many make the tooltip too big, we'll just use the latest.
contactDescriptions[give.agentDid] = give.description;
}
}
}
//console.log("Done retrieving receipts", contactConfirmed);
this.givenToMeDescriptions = contactDescriptions;
this.givenToMeConfirmed = contactConfirmed;
this.givenToMeUnconfirmed = contactUnconfirmed;
} else {
console.log(
"Got bad response status & data of",
resp.status,
resp.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your received time from the server.";
this.isAlertVisible = true;
}
const [givenByMeResp, givenToMeResp] = await Promise.all([
this.axios.get(givenByUrl, { headers }),
this.axios.get(givenToUrl, { headers }),
]);
const givenByMeDescriptions = {};
const givenByMeConfirmed = {};
const givenByMeUnconfirmed = {};
handleResponse(
givenByMeResp,
givenByMeDescriptions,
givenByMeConfirmed,
givenByMeUnconfirmed,
);
this.givenByMeDescriptions = givenByMeDescriptions;
this.givenByMeConfirmed = givenByMeConfirmed;
this.givenByMeUnconfirmed = givenByMeUnconfirmed;
const givenToMeDescriptions = {};
const givenToMeConfirmed = {};
const givenToMeUnconfirmed = {};
handleResponse(
givenToMeResp,
givenToMeDescriptions,
givenToMeConfirmed,
givenToMeUnconfirmed,
);
this.givenToMeDescriptions = givenToMeDescriptions;
this.givenToMeConfirmed = givenToMeConfirmed;
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
}
@@ -438,7 +396,7 @@ export default class ContactsView extends Vue {
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
allContacts,
);
}
@@ -449,7 +407,7 @@ export default class ContactsView extends Vue {
this.nameForDid(this.contacts, contact.did) +
" with DID " +
contact.did +
" ?"
" ?",
)
) {
await db.open();
@@ -463,21 +421,17 @@ export default class ContactsView extends Vue {
confirm(
"Are you sure you want to use one of your registrations for " +
this.nameForDid(this.contacts, contact.did) +
"?"
"?",
)
) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = await this.getIdentity(this.activeDid);
// Make a claim
const vcClaim: RegisterVerifiableCredential = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { identifier: identity.did },
object: SERVICE_ID,
recipient: { identifier: contact.did },
participant: { identifier: contact.did },
};
// Make a payload for the claim
const vcPayload = {
@@ -502,24 +456,24 @@ export default class ContactsView extends Vue {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const url = this.apiServer + "/api/v2/claim";
const headers = await this.getHeaders(identity);
try {
const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success?.handleId) {
if (resp.data?.success?.embeddedRecordError) {
this.alertTitle = "Registration Still Unknown";
let message = "There was some problem with the registration.";
if (typeof resp.data.success.embeddedRecordError == "string") {
message += " " + resp.data.success.embeddedRecordError;
}
this.alertMessage = message;
} else if (resp.data?.success?.handleId) {
contact.registered = true;
db.contacts.update(contact.did, { registered: true });
this.alertTitle = "Registration Success";
this.alertMessage = contact.name + " has been registered.";
this.isAlertVisible = true;
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
@@ -536,28 +490,18 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see.
this.alertTitle = "Error With Server";
this.alertMessage = userMessage;
this.isAlertVisible = true;
}
}
}
}
async setVisibility(contact: Contact, visibility: boolean) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
this.apiServer +
"/api/report/" +
(visibility ? "canSeeMe" : "cannotSeeMe");
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const identity = await this.getIdentity(this.activeDid);
const headers = await this.getHeaders(identity);
const payload = JSON.stringify({ did: contact.did });
try {
@@ -567,37 +511,24 @@ export default class ContactsView extends Vue {
db.contacts.update(contact.did, { seesMe: visibility });
} else {
this.alertTitle = "Error With Server";
console.log("Bad response setting visibility: ", resp.data);
console.error("Bad response setting visibility: ", resp.data);
if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message;
} else {
this.alertMessage = "Bad server response of " + resp.status;
}
this.isAlertVisible = true;
}
} catch (err) {
this.alertTitle = "Error With Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
async checkVisibility(contact: Contact) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
@@ -612,7 +543,6 @@ export default class ContactsView extends Vue {
" can " +
(visibility ? "" : "not ") +
"see your activity.";
this.isAlertVisible = true;
} else {
this.alertTitle = "Error With Server";
if (resp.data.error?.message) {
@@ -620,12 +550,10 @@ export default class ContactsView extends Vue {
} else {
this.alertMessage = "Bad server response of " + resp.status;
}
this.isAlertVisible = true;
}
} catch (err) {
this.alertTitle = "Error With Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
@@ -645,10 +573,7 @@ export default class ContactsView extends Vue {
}
async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = await this.getIdentity(this.activeDid);
// if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
@@ -657,7 +582,7 @@ export default class ContactsView extends Vue {
"There are " +
this.givenToMeUnconfirmed[fromDid] +
" unconfirmed hours from them." +
" Would you like to confirm some of those hours?"
" Would you like to confirm some of those hours?",
)
) {
this.$router.push({
@@ -670,15 +595,12 @@ export default class ContactsView extends Vue {
this.alertTitle = "Input Error";
this.alertMessage =
"This is not a valid number of hours: " + this.hourInput;
this.isAlertVisible = true;
} else if (!parseFloat(this.hourInput)) {
this.alertTitle = "Input Error";
this.alertMessage = "Giving 0 hours does nothing.";
this.isAlertVisible = true;
} else if (!identity) {
this.alertTitle = "Status Error";
this.alertMessage = "No identity is available.";
this.isAlertVisible = true;
} else {
// ask to confirm amount
let toFrom;
@@ -700,7 +622,7 @@ export default class ContactsView extends Vue {
" hours " +
toFrom +
description +
"?"
"?",
)
) {
this.createAndSubmitGive(
@@ -708,7 +630,7 @@ export default class ContactsView extends Vue {
fromDid,
toDid,
parseFloat(this.hourInput),
this.hourDescriptionInput
this.hourDescriptionInput,
);
}
}
@@ -719,7 +641,7 @@ export default class ContactsView extends Vue {
fromDid: string,
toDid: string,
amount: number,
description: string
description: string,
): Promise<void> {
// Make a claim
const vcClaim: GiveVerifiableCredential = {
@@ -756,21 +678,15 @@ export default class ContactsView extends Vue {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const url = this.apiServer + "/api/v2/claim";
const headers = await this.getHeaders(identity);
try {
const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success?.handleId) {
this.alertTitle = "Done";
this.alertMessage = "Successfully logged time to the server.";
this.isAlertVisible = true;
if (fromDid === identity.did) {
const newList = R.clone(this.givenByMeUnconfirmed);
newList[toDid] = (newList[toDid] || 0) + amount;
@@ -796,7 +712,6 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see.
this.alertTitle = "Error With Server";
this.alertMessage = userMessage;
this.isAlertVisible = true;
}
}
}
@@ -814,33 +729,6 @@ export default class ContactsView extends Vue {
}
}
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
public showGiveAmountsClassNames() {
return {
"bg-slate-500": this.showGiveTotals,

View File

@@ -1,47 +1,5 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<QuickNav selected="Discover"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
@@ -51,18 +9,20 @@
</h1>
<!-- Quick Search -->
<form id="QuickSearch" class="mb-4 flex">
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()">
<input
type="text"
v-model="searchTerms"
placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/>
<button
@click="search()"
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</button>
</form>
</div>
<!-- Result Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300">
@@ -70,95 +30,306 @@
<li>
<a
href="#"
class="inline-block py-3 rounded-t-lg border-b-2 active text-blue-600 border-blue-600 font-semibold"
@click="
projects = [];
searchLocal();
"
v-bind:class="computedLocalTabClassNames()"
>
Nearby
<span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
>20+</span
>{{ localCount }}</span
>
</a>
</li>
<li>
<a
href="#"
class="inline-block py-3 rounded-t-lg border-b-2 border-transparent hover:text-slate-600 hover:border-slate-300"
v-bind:class="computedRemoteTabClassNames()"
@click="
projects = [];
search();
"
>
Remote
<span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
>13</span
>{{ remoteCount }}</span
>
</a>
</li>
</ul>
</div>
<!-- Loading Animation -->
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<!-- Results List -->
<ul class="">
<li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4">
<div class="w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
<div class="grow">
<h2 class="text-base font-semibold">Canyon cleanup</h2>
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa> Rotary
<InfiniteScroll @reached-bottom="loadMoreData">
<ul>
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
</div>
</a>
</li>
<li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4">
<div class="w-12">
<img
src="https://picsum.photos/200/200?random=2"
class="w-full rounded"
/>
</div>
<div class="grow">
<h2 class="text-base font-semibold">Potluck with neighbors</h2>
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa> Andrew A.
<div class="grow">
<h2 class="text-base font-semibold">Canyon cleanup</h2>
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{ project.name }}
</div>
</div>
</div>
</a>
</li>
<li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4">
<div class="w-12">
<img
src="https://picsum.photos/200/200?random=3"
class="w-full rounded"
/>
</div>
<div class="grow">
<h2 class="text-base font-semibold">Historical site</h2>
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400 mr-1"></fa>
<em>Unknown</em>
</div>
</div>
</a>
</li>
</ul>
</a>
</li>
</ul>
</InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
@Options({
components: {},
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import InfiniteScroll from "@/components/InfiniteScroll";
@Component({
components: { AlertMessage, QuickNav, InfiniteScroll },
})
export default class DiscoverView extends Vue {}
export default class DiscoverView extends Vue {
activeDid = "";
apiServer = "";
searchTerms = "";
alertMessage = "";
alertTitle = "";
projects: ProjectData[] = [];
isLocalActive = true;
isRemoteActive = false;
localCount = 0;
remoteCount = 0;
isLoading = false;
async mounted() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.searchLocal();
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
public async search(beforeId?: string) {
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
console.log(beforeId);
if (beforeId) {
queryParams = queryParams + `&beforeId=${beforeId}`;
}
this.isRemoteActive = true;
this.isLocalActive = false;
try {
this.isLoading = true;
const response = await fetch(
this.apiServer + "/api/v2/report/plans?" + queryParams,
{
method: "GET",
headers: await this.buildHeaders(),
},
);
if (response.status !== 200) {
const details = await response.text();
throw details;
}
const results = await response.json();
const plans: ProjectData[] = results.data;
if (plans) {
for (const plan of plans) {
const { name, description, handleId = plan.handleId, rowid } = plan;
console.log("here");
this.projects.push({ name, description, handleId, rowid });
}
this.remoteCount = this.projects.length;
} else {
throw JSON.stringify(results);
}
} catch (e) {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving projects.";
this.alertTitle = "Error";
} finally {
this.isLoading = false;
}
}
public async searchLocal(beforeId?: string) {
const claimContents =
"claimContents=" + encodeURIComponent(this.searchTerms);
let queryParams = [
claimContents,
"minLocLat=40.901000",
"maxLocLat=40.904000",
"westLocLon=-111.914000",
"eastLocLon=-111.909000",
].join("&");
if (beforeId) {
queryParams = queryParams + `&beforeId=${beforeId}`;
}
try {
this.isLoading = true;
this.isLocalActive = true;
this.isRemoteActive = false;
const response = await fetch(
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
{
method: "GET",
headers: await this.buildHeaders(),
},
);
if (response.status !== 200) {
throw await response.text();
}
const results = await response.json();
if (results.data) {
if (beforeId) {
const plans: ProjectData[] = results.data;
for (const plan of plans) {
const { name, description, handleId = plan.handleId, rowid } = plan;
if (beforeId !== plan["rowid"]) {
this.projects.push({ name, description, handleId, rowid });
}
}
} else {
this.projects = results.data;
}
this.localCount = this.projects.length;
} else {
throw JSON.stringify(results);
}
} catch (e) {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving projects.";
this.alertTitle = "Error";
} finally {
this.isLoading = false;
}
}
/**
* Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load
**/
async loadMoreData(payload: boolean) {
if (this.projects.length > 0 && payload) {
const latestProject = this.projects[this.projects.length - 1];
console.log("rowid", latestProject, payload);
console.log(Object.keys(latestProject));
if (this.isLocalActive) {
this.searchLocal(latestProject["rowid"]);
} else if (this.isRemoteActive) {
this.search(latestProject["rowid"]);
}
}
}
/**
* Handle clicking on a project entry found in the list
* @param id of the project
**/
onClickLoadProject(id: string) {
localStorage.setItem("projectId", id);
const route = {
name: "project",
};
this.$router.push(route);
}
public computedLocalTabClassNames() {
return {
"inline-block": true,
"py-3": true,
"rounded-t-lg": true,
"border-b-2": true,
active: this.isLocalActive,
"text-blue-600": this.isLocalActive,
"border-blue-600": this.isLocalActive,
"font-semibold": this.isLocalActive,
"border-transparent": !this.isLocalActive,
"hover:text-slate-600": !this.isLocalActive,
"hover:border-slate-300": !this.isLocalActive,
};
}
public computedRemoteTabClassNames() {
return {
"inline-block": true,
"py-3": true,
"rounded-t-lg": true,
"border-b-2": true,
active: this.isRemoteActive,
"text-blue-600": this.isRemoteActive,
"border-blue-600": this.isRemoteActive,
"font-semibold": this.isRemoteActive,
"border-transparent": !this.isRemoteActive,
"hover:text-slate-600": !this.isRemoteActive,
"hover:border-slate-300": !this.isRemoteActive,
};
}
}
</script>

View File

@@ -1,52 +1,5 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-400">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
@@ -55,16 +8,53 @@
</h1>
<div>
<h2 class="text-xl font-semibold">Introduction</h2>
<p>
This app is a window into data that you and your friends own, focused on
gifts and collaboration.
</p>
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<p>
We are building networks of people who want to grow a gifting society.
First of all, you can record ways you've seen people give, and that
leaves a permanent record -- one that came from you, and the recipient
can prove it was for them. This is personally gratifying, but it extends
to broader work: volunteers can get confirmation of activity and
selectively show off their contributions and network.
</p>
<p>
You can also record projects and plans and invite others to collaborate.
Soon you'll be able to see when others are interested and see how much
they're willing to contribute, even if there are conditions.
</p>
<p>
This app uses the power of cryptography to build a reputation, recording
activity that you can share at your discretion. You put some activity
public, but your sensitive information is not shared with anyone,
including our services. This is in contrast to Meta and Google, who hold
your data and allow you use it. Those services are useful, but they have
the control; this app gives you the control.
</p>
<h2 class="text-xl font-semibold">How do I take my first action?</h2>
<p>
You need someone to register you -- usually the person who told you
about this app, on the Contacts
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
and after you have contacts, you can select any contact on the home page
and record your appreciation for... whatever. That is a claim recorded
on a custom ledger. The day after being registered, you'll be able to
register others; later, you can create projects, too.
</p>
<p>
Note that there are limits to how many each person can register, so you
may have to wait.
</p>
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
<p>
There are two parts to backup your data: the identifier secrets and the
other data such as settings, contacts, etc.
There are two sets of data to backup: the identifier secrets and the
other data that isn't quite a secret such as settings, contacts, etc.
</p>
<div class="px-4">
@@ -78,6 +68,10 @@
<li>
Click on "Backup Identifier Seed" and follow the instructions.
</li>
<li>
If you have other identifiers, switch to each one and repeat those
steps.
</li>
</ul>
<h2 class="text-xl font-semibold">
@@ -107,23 +101,10 @@
</h2>
<ul class="list-disc list-inside">
<li>
You only have one identifier at a time. If you have an identifier on
Your Identity <fa icon="circle-user" class="fa-fw" /> page, you'll
need to clear it out;
<a
href="https://www.lifewire.com/how-to-clear-cache-2617980"
class="text-blue-500"
>
here are some helpful instructions.
</a>
But beware! This will also clear out your settings and contact data,
so be sure to back that up first.
</li>
<li>
<router-link class="text-blue-500" to="/">
Go to the start
<router-link class="text-blue-500" to="/import-account">
Go to the import page
</router-link>
and choose "Yes" to enter the identity you backed up.
and enter the seed phrase you backed up.
</li>
</ul>
@@ -146,6 +127,64 @@
may also add their public key by adding another comma followed by the
key.
</p>
<h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info?
</h2>
<p>
If you don't see anything associated with a person, this is typically
because they have not given you permission to see their information. Ask
them to add you to their contact list and make sure the eye next to your
name is open like this
<fa icon="eye" class="fa-fw" /> and not closed like this
<fa icon="eye-slash" class="fa-fw" />.
</p>
<p>
Sometimes the reason you don't see something is because the search time
is limited. Go to the bottom and make sure to load all the data on a
list. If you still don't see it, try a search or view on a different
page.
</p>
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
<p>
Go
<router-link to="start" class="text-blue-500">
create another identity here.
</router-link>
</p>
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
<p>
See
<a href="https://endorser.ch/privacy-policy" class="text-blue-500">
the Endorser Service Privacy Policy.
</a>
</p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<p>
{{ package.version }}
</p>
<h2 class="text-xl font-semibold">
For any other questions, including remove your data:
</h2>
<p>
Contact us through
<a href="https://communitycred.org">CommunityCred.org</a>.
</p>
</div>
</section>
</template>
<script lang="ts">
import * as Package from "../../package.json";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav";
@Component({ components: { QuickNav } })
export default class Help extends Vue {
package = Package;
}
</script>

View File

@@ -1,15 +1,371 @@
<template>
<section></section>
<QuickNav selected="Home"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Time Safari
</h1>
<div class="mb-8">
<h2 class="text-xl font-bold">Quick Action</h2>
<p class="mb-4">Show appreciation to a contact:</p>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
>
<div class="mb-1">
<fa icon="user" class="fa-fw fa-xl text-slate-400"></fa>
</div>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ contact.name || "(no name)" }}
</h3>
</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) -->
<router-link
v-if="allContacts.length > 7"
:to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
>
Show More Contacts&hellip;
</router-link>
<!-- If there are no contacts, show this instead: -->
<div
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
>
(No contacts to show.)
</div>
</div>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
<div :class="{ hidden: isHiddenSpinner }">
<p class="text-slate-500 text-center italic mt-4 mb-4">
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</p>
</div>
<ul class="border-t border-slate-300">
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId"
>
You've seen all claims below:
</div>
<div class="flex">
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
<!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
<span class="">{{ this.giveDescription(record) }}</span>
</div>
</li>
</ul>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Options({
components: {
HelloWorld,
},
@Component({
components: { GiftedDialog, AlertMessage, QuickNav },
})
export default class HomeView extends Vue {}
export default class HomeView extends Vue {
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
feedAllLoaded = false;
feedData = [];
feedPreviousOldestId = null;
feedLastViewedId = null;
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() {
try {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed();
} catch (err) {
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
}
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
public async updateAllFeed() {
this.isHiddenSpinner = false;
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data);
this.feedAllLoaded = results.hitLimit;
this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId;
if (
this.feedLastViewedId == null ||
this.feedLastViewedId < results.data[0].jwtId
) {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId,
});
// but not for this page because we need to remember what it was before
}
}
})
.catch((e) => {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving feed data.";
this.alertTitle = "Error";
});
this.isHiddenSpinner = true;
}
public async retrieveClaims(endorserApiServer, identifier, beforeId) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch(
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
{
method: "GET",
headers: await this.buildHeaders(),
},
);
if (response.status !== 200) {
throw await response.text();
}
const results = await response.json();
if (results.data) {
return results;
} else {
throw JSON.stringify(results);
}
}
giveDescription(giveRecord) {
let claim = giveRecord.fullClaim;
if (claim.claim) {
claim = claim.claim;
}
// agent.did is for legacy data, before March 2023
const giverDid =
claim.agent?.identifier || claim.agent?.did || giveRecord.issuer;
const giverInfo = didInfo(
giverDid,
this.activeDid,
this.allMyDids,
this.allContacts,
);
const gaveAmount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.description || "something unknown";
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientInfo = gaveRecipientId
? " to " +
didInfo(
gaveRecipientId,
this.activeDid,
this.allMyDids,
this.allContacts,
)
: "";
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
}
displayAmount(code, amt) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
}
currencyShortWordForCode(unitCode, single) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
openDialog(giver) {
this.$refs.customDialog.open(giver);
}
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
});
} else {
// action was "cancel" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(giverDid, description, hours) {
if (!this.activeDid) {
this.setAlert(
"Error",
"You must select an identity before you can record a give.",
);
return;
}
if (!description && !hours) {
this.setAlert(
"Error",
"You must enter a description or some number of hours.",
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
);
if (this.isGiveCreationError(result)) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give result:", result);
this.setAlert(
"Error",
errorMessage || "There was an error recording the give.",
);
} else {
this.setAlert("Success", "That gift was recorded.");
}
} catch (error) {
console.log("Error with give caught:", error);
this.setAlert(
"Error",
this.getGiveErrorMessage(error) ||
"There was an error recording the give.",
);
}
}
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability
isGiveCreationError(result) {
return result.status !== 201 || result.data?.error;
}
getGiveCreationErrorMessage(result) {
return result.data?.error?.message;
}
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
}
}
</script>

View File

@@ -0,0 +1,169 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa>
</router-link>
Switch Identity
</h1>
</div>
<!-- Identity List -->
<!-- Current Identity - Display First! -->
<div class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4">
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
<span class="overflow-hidden">
<h2 class="text-xl font-semibold mb-0">
{{ firstName }} {{ lastName }}
</h2>
<div class="text-sm text-slate-500 truncate">
<b>ID:</b> <code>{{ activeDid }}</code>
</div>
</span>
</div>
<!-- Other Identity/ies -->
<ul class="mb-4">
<li
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
v-for="ident in otherIdentities"
:key="ident.did"
@click="switchAccount(ident.did)"
>
<fa icon="circle" class="fa-fw text-slate-400 text-xl mr-3"></fa>
<span class="overflow-hidden">
<h2 class="text-xl font-semibold mb-0"></h2>
<div class="text-sm text-slate-500 truncate">
<b>ID:</b> <code>{{ ident.did }}</code>
</div>
</span>
</li>
</ul>
<!-- Actions -->
<router-link
:to="{ name: 'start' }"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
>
Add Another Identity&hellip;
</router-link>
<a
href="#"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
@click="switchAccount('0')"
>
No Identity
</a>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, QuickNav } })
export default class IdentitySwitcherView extends Vue {
Constants = AppString;
public accounts: AccountsSchema;
public activeDid;
public firstName;
public lastName;
public alertTitle;
public alertMessage;
public otherIdentities = [];
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
return identity;
}
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "No";
this.lastName = settings?.lastName || "Name";
this.showContactGives = !!settings?.showContactGivesInline;
const identity = await this.getIdentity(this.activeDid);
if (identity) {
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
}
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
for (let n = 0; n < accounts.length; n++) {
const did = JSON.parse(accounts[n].identity)["did"];
if (did && this.activeDid !== did) {
this.otherIdentities.push({ did: did });
}
}
} catch (err) {
if (
err.message ===
"Attempted to load account records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.alertMessage =
"Clear your cache and start over (after data backup).";
console.error(
"Telling user to clear cache at page create because:",
err,
);
this.alertTitle = "Error Creating Account";
}
}
}
async switchAccount(did: string) {
// 0 means none
if (did === "0") {
did = undefined;
}
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did,
});
this.activeDid = did;
this.otherIdentities = [];
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
for (let n = 0; n < accounts.length; n++) {
const did = JSON.parse(accounts[n].identity)["did"];
if (did && this.activeDid !== did) {
this.otherIdentities.push({ did: did });
}
}
this.$router.push({ name: "account" });
}
}
</script>

View File

@@ -43,12 +43,12 @@
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
import { deriveAddress, newIdentifier } from "../libs/crypto";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Options({
@Component({
components: {},
})
export default class ImportAccountView extends Vue {
@@ -72,7 +72,7 @@ export default class ImportAccountView extends Vue {
this.address,
this.publicHex,
this.privateHex,
this.derivationPath
this.derivationPath,
);
try {
@@ -93,8 +93,7 @@ export default class ImportAccountView extends Vue {
});
this.$router.push({ name: "account" });
} catch (err) {
console.log("Error!");
console.log(err);
console.error("Error saving mnemonic & updating settings:", err);
}
}
}

View File

@@ -13,47 +13,46 @@
[New/Edit] Identity
</h1>
</div>
<form>
<input
type="text"
placeholder="First Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="firstName"
/>
<input
type="text"
placeholder="Last Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="lastName"
/>
<div class="mt-8">
<button
type="button"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
@click="onClickSaveChanges()"
>
Save Changes
</button>
<!-- SHOW ME instead while processing saving changes -->
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="onClickCancel()"
>
Cancel
</button>
</div>
</form>
<input
type="text"
placeholder="First Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="firstName"
/>
<input
type="text"
placeholder="Last Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="lastName"
/>
<div class="mt-8">
<button
type="button"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
@click="onClickSaveChanges()"
>
Save Changes
</button>
<!-- SHOW ME instead while processing saving changes -->
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="onClickCancel()"
>
Cancel
</button>
</div>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
import { db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Options({
@Component({
components: {},
})
export default class NewEditAccountView extends Vue {

View File

@@ -16,54 +16,51 @@
</div>
<!-- Project Details -->
<form>
<select
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
<select class="block w-full rounded border border-slate-400 mb-4 px-3 py-2">
<option disabled>Choose a commitment type</option>
<option selected>Time</option>
<option>Cryptocurrency</option>
<option>Money</option>
</select>
<!-- Time amount -->
<div class="mb-4 flex items-stretch">
<input
type="number"
placeholder="0.0"
class="block w-full rounded-l border border-slate-400 px-3 py-2"
/>
<span
class="px-4 py-2 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>hours</span
>
<option disabled>Choose a commitment type</option>
<option selected>Time</option>
<option>Cryptocurrency</option>
<option>Money</option>
</select>
</div>
<!-- Time amount -->
<div class="mb-4 flex items-stretch">
<input
type="number"
placeholder="0.0"
class="block w-full rounded-l border border-slate-400 px-3 py-2"
/>
<span
class="px-4 py-2 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>hours</span
>
</div>
<!-- Crypto amount -->
<!-- Crypto amount -->
<!-- Money amount -->
<!-- Money amount -->
<div class="mt-8">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Commit"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Maybe Later
</button>
</div>
</form>
<div class="mt-8">
<input
type="submit"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
value="Commit"
/>
<button
type="button"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
>
Maybe Later
</button>
</div>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
@Options({
@Component({
components: {},
})
export default class NewEditCommitmentView extends Vue {}

View File

@@ -49,8 +49,9 @@
<span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }"
><i class="fa-solid fa-spinner fa-spin-pulse"></i>
<span :class="{ hidden: isHiddenSpinner }">
<!-- icon no worky? -->
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving&hellip;</span
>
</button>
@@ -62,79 +63,96 @@
Cancel
</button>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core";
import AlertMessage from "@/components/AlertMessage";
interface VerifiableCredential {
"@context": string;
"@type": string;
name: string;
description: string;
identifier?: string;
}
@Options({
components: {},
@Component({
components: { AlertMessage },
})
export default class NewEditProjectView extends Vue {
activeDid = "";
apiServer = "";
projectName = "";
description = "";
errorMessage = "";
numAccounts = 0;
alertTitle = "";
alertMessage = "";
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
projectId = localStorage.getItem("projectId") || "";
isHiddenSave = false;
isHiddenSpinner = true;
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.projectId) {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
if (this.numAccounts === 0) {
console.error("Error: no account was found.");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = await this.getIdentity(this.activeDid);
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
this.LoadProject(identity);
}
}
}
async LoadProject(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const token = await accessToken(identity);
@@ -145,14 +163,13 @@ export default class NewEditProjectView extends Vue {
try {
const resp = await this.axios.get(url, { headers });
console.log(resp.status, resp.data);
if (resp.status === 200) {
const claim = resp.data.claim;
this.projectName = claim.name;
this.description = claim.description;
}
} catch (error) {
console.log(error);
console.error("Got error retrieving that project", error);
}
}
@@ -191,8 +208,7 @@ export default class NewEditProjectView extends Vue {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@@ -201,7 +217,6 @@ export default class NewEditProjectView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
console.log("Got resp data:", resp.data);
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
@@ -211,35 +226,37 @@ export default class NewEditProjectView extends Vue {
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
useAppStore().setProjectId(
resp.data.success.handleId || resp.data.success.fullIri
resp.data.success.handleId || resp.data.success.fullIri,
);
setTimeout(
function (that: Vue) {
const route = {
name: "project",
};
console.log(route);
that.$router.push(route);
},
2000,
this
this,
);
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError;
if (serverError) {
this.isAlertVisible = true;
if (serverError.message) {
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError);
this.alertTitle = "User Message";
userMessage = serverError.message; // This is info for the user.
userMessage = serverError.response.data.error.message; // This is info for the user.
this.alertMessage = userMessage;
} else {
this.alertTitle = "Server Message";
this.alertMessage = JSON.stringify(serverError.toJSON());
}
} else {
console.log("Here's the full error trying to save the claim:", error);
console.error(
"Here's the full error trying to save the claim:",
error,
);
this.alertTitle = "Claim Error";
this.alertMessage = error as string;
}
@@ -249,23 +266,14 @@ export default class NewEditProjectView extends Vue {
}
}
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public async onSaveProjectClick() {
this.isHiddenSave = true;
this.isHiddenSpinner = false;
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
if (this.numAccounts === 0) {
console.error("Error: there is no account.");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = await this.getIdentity(this.activeDid);
this.SaveProject(identity);
}
}
@@ -273,23 +281,5 @@ export default class NewEditProjectView extends Vue {
public onCancelClick() {
this.$router.back();
}
isAlertVisible = false;
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>

View File

@@ -0,0 +1,81 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Identity
</h1>
<div class="flex justify-center py-12">
<span />
<span v-if="loading">
<span class="text-xl">Creating...&nbsp;</span>
<fa
icon="spinner"
class="fa-spin fa-spin-pulse"
color="green"
size="128"
></fa>
</span>
<span v-else>
<span class="text-xl">Created!</span>
<fa
icon="burst"
class="fa-beat px-12"
color="green"
style="
--fa-animation-duration: 1s;
--fa-animation-direction: reverse;
--fa-animation-iteration-count: 1;
--fa-beat-scale: 6;
"
></fa>
</span>
<span />
</div>
</section>
</template>
<script lang="ts">
import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav";
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {
loading = true;
async mounted() {
const mnemonic = generateSeed();
// address is 0x... ETH address, without "did:eth:"
const [address, privateHex, publicHex, derivationPath] =
deriveAddress(mnemonic);
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
const identity = JSON.stringify(newId);
await accountsDB.open();
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: derivationPath,
did: newId.did,
identity: identity,
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
this.loading = false;
setTimeout(() => {
this.$router.push({ name: "account" });
}, 1000);
}
}
</script>

View File

@@ -1,48 +1,5 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="hand" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<QuickNav selected="Projects"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
@@ -66,16 +23,15 @@
</h1>
</div>
<div>
{{ errorMessage }}
</div>
<!-- Project Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div>
<h2 class="text-xl font-semibold">{{ name }}</h2>
<div class="flex justify-between gap-4 text-sm mb-3">
<span><fa icon="user" class="fa-fw text-slate-400"></fa> Rotary</span>
<span
><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ issuer }}</span
>
<span
><fa icon="calendar" class="fa-fw text-slate-400"></fa
>{{ timeSince }}
@@ -108,77 +64,191 @@
</button>
</div>
<!-- Commit -->
<router-link
:to="{ name: 'new-edit-commitment' }"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
>Make Commitment</router-link
>
<!-- Commitments -->
<div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">Commitments</h3>
<ul class="text-sm border-t border-slate-300">
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
<span>[Username]</span>
<span
>5 hours <fa icon="spinner" class="fa-fw text-slate-400"></fa
></span>
</li>
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
<span>[Username]</span>
<span
>US$ 20.00 <fa icon="circle-check" class="fa-fw text-lime-500"></fa
></span>
</li>
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
<span>[Username]</span>
<span
>0.1 BTC <fa icon="spinner" class="fa-fw text-slate-400"></fa
></span>
</li>
</ul>
<div>
<div v-if="activeDid">
<button
@click="openDialog({ name: 'you', did: activeDid })"
class="text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
>
I gave...
</button>
&horbar; or:
</div>
<!-- similar contact selection code is in multiple places -->
Record a gift from
<span v-for="contact in allContacts" :key="contact.did">
<button @click="openDialog(contact)" class="text-blue-500">
&nbsp;{{ contact.name }}</button
>,
</span>
<span v-if="allContacts.length > 0">&nbsp;or&nbsp;</span>
<button @click="openDialog()" class="text-blue-500">
someone not specified
</button>
</div>
<!-- Gifts to & from this -->
<div class="mt-8 flex justify-around">
<div>
<h1 class="text-xl">Given to this Project</h1>
</div>
<div>
<h1 class="text-xl">... and from this Project</h1>
</div>
</div>
<div class="flex justify-around">
<div class="w-1/2">
<div v-for="give in givesToThis" :key="give.id">
<div class="flex justify-between">
<div class="flex gap-3">
<div class="flex gap-2">
<fa icon="user" class="fa-fw text-slate-400"></fa>
<span>{{
didInfo(give.agentDid, activeDid, allMyDids, allContacts)
}}</span>
</div>
<div class="flex gap-2" v-if="give.amount">
<fa icon="coins" class="fa-fw text-slate-400"></fa>
<span>{{ give.amount }}</span>
</div>
<div class="flex gap-2" v-if="give.description">
<fa icon="comment" class="fa-fw text-slate-400"></fa>
<span>{{ give.description }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="w-1/2">
<div v-for="give in givesByThis" :key="give.id">
<div class="flex justify-between">
<div class="flex gap-3">
<div class="flex gap-2">
<fa icon="user" class="fa-fw text-slate-400"></fa>
<span>{{
didInfo(give.agentDid, activeDid, allMyDids, allContacts)
}}</span>
</div>
<div class="flex gap-2" v-if="give.amount">
<fa icon="coins" class="fa-fw text-slate-400"></fa>
<span>{{ give.amount }}</span>
</div>
<div class="flex gap-2">
<fa icon="comment" class="fa-fw text-slate-400"></fa>
<span>{{ give.description }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { AxiosError } from "axios";
import * as moment from "moment";
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import {
createAndSubmitGive,
didInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Options({
components: {},
@Component({
components: { GiftedDialog, AlertMessage, QuickNav },
})
export default class ProjectViewView extends Vue {
expanded = false;
name = "";
activeDid = "";
alertMessage = "";
alertTitle = "";
allMyDids: Array<string> = [];
allContacts: Array<Contact> = [];
apiServer = "";
description = "";
expanded = false;
givesToThis: Array<GiveServerRecord> = [];
givesByThis: Array<GiveServerRecord> = [];
name = "";
issuer = "";
projectId = localStorage.getItem("projectId") || ""; // handle ID
timeSince = "";
truncatedDesc = "";
truncateLength = 40;
timeSince = "";
projectId = localStorage.getItem("projectId") || "";
errorMessage = "";
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const accounts = accountsDB.accounts;
const accountsArr = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr?.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
this.LoadProject(identity);
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
onEditClick() {
localStorage.setItem("projectId", this.projectId as string);
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
// Isn't there a better way to make this available to the template?
didInfo(did, activeDid, dids, contacts) {
return didInfo(did, activeDid, dids, contacts);
}
expandText() {
this.expanded = true;
}
@@ -188,18 +258,20 @@ export default class ProjectViewView extends Vue {
}
async LoadProject(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// eslint-disable-next-line prettier/prettier
const url = endorserApiServer + "/api/claim/byHandle/" + encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const url =
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try {
const resp = await this.axios.get(url, { headers });
console.log("resp.status, resp.data", resp.status, resp.data);
if (resp.status === 200) {
const startTime = resp.data.startTime;
if (startTime != null) {
@@ -207,41 +279,134 @@ export default class ProjectViewView extends Vue {
const now = moment.now();
this.timeSince = moment.utc(now).to(eventDate);
}
this.issuer = resp.data.issuer;
this.name = resp.data.claim?.name || "(no name)";
this.description = resp.data.claim?.description || "(no description)";
this.truncatedDesc = this.description.slice(0, this.truncateLength);
} else if (resp.status === 404) {
// actually, axios throws an error so we never get here
this.errorMessage = "That project does not exist.";
this.alertMessage = "That project does not exist.";
}
} catch (error: unknown) {
const serverError = error as AxiosError;
if (serverError.response?.status === 404) {
this.errorMessage = "That project does not exist.";
this.alertMessage = "That project does not exist.";
} else {
this.errorMessage =
this.alertMessage =
"Something went wrong retrieving that project." +
" See logs for more info.";
console.log("Error retrieving project:", error);
console.error("Error retrieving project:", serverError.message);
}
}
const givesInUrl =
this.apiServer +
"/api/v2/report/givesForPlans?planIds=" +
encodeURIComponent(JSON.stringify([this.projectId]));
try {
const resp = await this.axios.get(givesInUrl, { headers });
if (resp.status === 200 && resp.data.data) {
this.givesToThis = resp.data.data;
} else {
this.alertMessage = "Failed to retrieve gives to this project.";
}
} catch (error: unknown) {
const serverError = error as AxiosError;
this.alertMessage =
"Something went wrong retrieving gives to this project.";
console.error(
"Error retrieving gives to this project:",
serverError.message,
);
}
const givesOutUrl =
this.apiServer +
"/api/v2/report/givesProvidedBy?providerId=" +
encodeURIComponent(this.projectId);
try {
const resp = await this.axios.get(givesOutUrl, { headers });
if (resp.status === 200 && resp.data.data) {
this.givesByThis = resp.data.data;
} else {
this.alertMessage = "Failed to retrieve gives by this project.";
}
} catch (error: unknown) {
const serverError = error as AxiosError;
this.alertMessage = "Something went wrong retrieving gives by project.";
console.error(
"Error retrieving gives by this project:",
serverError.message,
);
}
}
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
openDialog(contact) {
this.$refs.customDialog.open(contact);
}
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
});
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProject(identity);
// action was not "confirm" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
async recordGive(giverDid, description, hours) {
if (!this.activeDid) {
this.alertTitle = "Error";
this.alertMessage =
"You must select an identity before you can record a give.";
return;
}
if (!description && !hours) {
this.alertTitle = "Error";
this.alertMessage =
"You must enter a description or some number of hours.";
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
this.projectId,
);
if (result.status !== 201 || result.data?.error) {
console.log("Error with give result:", result);
this.alertTitle = "Error";
this.alertMessage =
result.data?.error?.message ||
"There was an error recording the give.";
} else {
this.alertTitle = "Success";
this.alertMessage = "That gift was recorded.";
}
} catch (e) {
console.log("Error with give caught:", e);
this.alertTitle = "Error";
this.alertMessage =
e.userMessage ||
e.response?.data?.error?.message ||
"There was an error recording the give.";
}
}
}

View File

@@ -1,47 +1,5 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<QuickNav selected="Projects"></QuickNav>
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
@@ -49,18 +7,17 @@
</h1>
<!-- Quick Search -->
<form id="QuickSearch" class="mb-4 flex">
<input
type="text"
placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/>
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</button>
</form>
<input
type="text"
placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/>
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</button>
<!-- New Project -->
<button
@@ -70,116 +27,195 @@
<fa icon="plus" class="fa-fw"></fa>
</button>
<!-- Results List -->
<ul class="">
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
<!-- Loading Animation -->
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm truncate">
{{ project.description }}
<!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData">
<ul>
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
</div>
</a>
</li>
</ul>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm truncate">
{{ project.description }}
</div>
</div>
</a>
</li>
</ul>
</InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Options({
components: {},
@Component({
components: { InfiniteScroll, AlertMessage, QuickNav },
})
export default class ProjectsView extends Vue {
projects: { handleId: string; name: string; description: string }[] = [];
apiServer = "";
projects: ProjectData[] = [];
current: IIdentifier;
isLoading = false;
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
/**
* Core project data loader
* @param url the url used to fetch the data
* @param token Authorization token
**/
async dataLoader(url: string, token: string) {
const headers: { [key: string]: string } = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
try {
this.isLoading = true;
const resp = await this.axios.get(url, { headers });
if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data;
for (const plan of plans) {
const { name, description, handleId = plan.fullIri, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
}
} else {
console.log("Bad server response & data:", resp.status, resp.data);
throw Error("Failed to get projects from the server.");
}
} catch (error) {
console.error("Got error loading projects:", error.message);
this.alertTitle = "Error";
this.alertMessage = "Got an error loading projects:" + error.message;
} finally {
this.isLoading = false;
}
}
/**
* Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load
**/
async loadMoreData(payload: boolean) {
if (this.projects.length > 0 && payload) {
const latestProject = this.projects[this.projects.length - 1];
const url = `${this.apiServer}/api/v2/report/plansByIssuer?beforeId=${latestProject.rowid}`;
const token = await accessToken(this.current);
await this.dataLoader(url, token);
}
}
/**
* Handle clicking on a project entry found in the list
* @param id of the project
**/
onClickLoadProject(id: string) {
console.log("projectId", id);
localStorage.setItem("projectId", id);
const route = {
name: "project",
};
console.log(route);
this.$router.push(route);
}
/**
* Load projects initially
* @param identity of the user
**/
async LoadProjects(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/report/plansByIssuer";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const plans = resp.data.data;
for (let i = 0; i < plans.length; i++) {
const plan = plans[i];
const data = {
name: plan.name,
description: plan.description,
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
handleId: plan.handleId || plan.fullIri,
};
this.projects.push(data);
}
}
} catch (error) {
console.log(error);
}
const url = `${this.apiServer}/api/v2/report/plansByIssuer`;
const token: string = await accessToken(identity);
await this.dataLoader(url, token);
}
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
public async getIdentity(activeDid) {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProjects(identity);
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
/**
* 'created' hook runs when the Vue instance is first created
**/
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.numAccounts === 0) {
console.error("No accounts found.");
this.alertTitle = "Error";
this.alertMessage = "You need an identity to load your projects.";
} else {
const identity = await this.getIdentity(activeDid);
this.current = identity;
this.LoadProjects(identity);
}
} catch (err) {
console.log("Error initializing:", err);
this.alertTitle = "Error";
this.alertMessage = "Something went wrong loading your projects.";
}
}
/**
* Handling clicking on the new project button
**/
onClickNewProject(): void {
localStorage.removeItem("projectId");
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
}

View File

@@ -0,0 +1,84 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Seed Backup
</h1>
<div class="flex justify-between py-2">
<span />
<span>
<router-link
:to="{ name: 'help' }"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
>
Help
</router-link>
</span>
</div>
<div v-if="activeAccount">
<p>
BEWARE: Anyone who gets hold of this mnemonic seed phrase will be able
impersonate you and take over any digital holdings based on it. So only
reveal it when you are in a private place out of sight of other eyes,
and only record it in something private -- don't take a screenshot or
send it to any online service.
</p>
<button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="showSeedPhrase"
>
Click here when you're ready to see it.
</button>
<p v-if="showSeed">{{ activeAccount.mnemonic }}</p>
</div>
<div v-else>You do not have an active identity.</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import * as R from "ramda";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, QuickNav } })
export default class SeedBackupView extends Vue {
activeAccount = null;
showSeed = false;
alertMessage = "";
alertTitle = "";
// 'created' hook runs when the Vue instance is first created
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
} catch (err) {
console.error("Got an error loading an identity:", err);
this.alertTitle = "Error Loading Account";
this.alertMessage = "Got an error loading your seed data.";
}
}
showSeedPhrase() {
this.showSeed = true;
}
}
</script>

View File

@@ -28,14 +28,14 @@
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
@Options({
@Component({
components: {},
})
export default class StartView extends Vue {
public onClickYes() {
this.$router.push({ name: "account" });
this.$router.push({ name: "new-identifier" });
}
public onClickNo() {

View File

@@ -0,0 +1,103 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Achievements & Statistics
</h1>
<div>
Here is a view of the activity you can see.
<ul class="list-disc list-inside">
<li>Each identity and claim has a unique position.</li>
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
<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>
<div class="mt-3">
<div v-if="worldProperties.startTime">
<label>Time Range:&nbsp;</label>
{{ worldProperties.startTime }}
-
{{ worldProperties.endTime }}
</div>
<div v-if="worldProperties.animationDurationSeconds">
<label>Animation Time:&nbsp;</label>
{{ worldProperties.animationDurationSeconds }} seconds
</div>
</div>
<button class="float-right" @click="captureGraphics()">Screenshot</button>
<div id="scene-container" class="h-screen"></div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator";
import { World } from "@/components/World/World.js";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, World, QuickNav } })
export default class StatisticsView extends Vue {
world: World;
worldProperties: WorldProperties = {};
alertTitle = "";
alertMessage = "";
mounted() {
try {
const container = document.querySelector("#scene-container");
const newWorld = new World(container, this);
newWorld.start();
this.world = newWorld;
} catch (err) {
console.log(err);
this.alertTitle = "Mounting error";
this.alertMessage = err.message;
}
}
public captureGraphics() {
/**
* This yields an SVG that only shows white and black highlights
// from https://stackoverflow.com/questions/27632621/exporting-from-three-js-scene-to-svg-or-other-vector-format
**/
const rendererSVG = new SVGRenderer();
rendererSVG.setSize(window.innerWidth, window.innerHeight);
rendererSVG.render(this.world.scene, this.world.camera);
ExportToSVG(rendererSVG, "test.svg");
}
public setWorldProperty(propertyName, propertyValue) {
this.worldProperties[propertyName] = propertyValue;
}
}
function ExportToSVG(rendererSVG, filename) {
const XMLS = new XMLSerializer();
const svgfile = XMLS.serializeToString(rendererSVG.domElement);
const svgData = svgfile;
const preface = '<?xml version="1.0" standalone="no"?>\r\n';
const svgBlob = new Blob([preface, svgData], {
type: "image/svg+xml;charset=utf-8",
});
const svgUrl = URL.createObjectURL(svgBlob);
const downloadLink = document.createElement("a");
downloadLink.href = svgUrl;
downloadLink.download = filename;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
</script>

View File

@@ -7,4 +7,9 @@ module.exports = defineConfig({
topLevelAwait: true,
},
},
pwa: {
iconPaths: {
faviconSVG: "img/icons/safari-pinned-tab.svg",
},
},
});