Compare commits

..

110 Commits

Author SHA1 Message Date
ac4c92d8e8 Merge branch 'master' into update-api-server 2023-08-07 03:13:26 -04:00
937a3cb6c6 Merge pull request 'Minor cleanup' (#54) from seed-backup-view-improvements into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#54
2023-08-07 03:11:23 -04:00
Matthew Aaron Raymer
304985f88d Merge remote-tracking branch 'origin/polish-ui-project-view' 2023-08-07 16:08:48 +08:00
Matthew Aaron Raymer
9a41aff8f0 Merge remote-tracking branch 'origin/jdenticon-entity-photos' 2023-08-07 15:59:47 +08:00
Matthew Aaron Raymer
e19cd980b4 Merging with corrections 2023-08-07 15:54:06 +08:00
194f741984 Merge branch 'master' into seed-backup-view-improvements 2023-08-07 03:11:07 -04:00
b31c0d975c Merge pull request 'fix display of gives on contact screen; adjust give UI for project' (#49) from ui-fix into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#49
2023-08-07 02:42:30 -04:00
f09684d7cd Merge branch 'master' into ui-fix 2023-08-07 02:41:30 -04:00
1767a48a7f Merge pull request 'New notification system + test' (#48) from notiwind-alert into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#48
2023-08-07 02:41:05 -04:00
d2e2fc707e Merge branch 'master' into notiwind-alert 2023-08-07 02:34:22 -04:00
bf6830a1a8 update URL for API server 2023-08-05 18:20:03 -06:00
Jose Olarte III
fe09f5180d Minor cleanup 2023-08-03 20:12:47 +08:00
Jose Olarte III
4de66b1609 Cleaned up Project view UI 2023-07-28 20:22:03 +08:00
Jose Olarte III
4b87692231 Added jdenticon to more views 2023-07-25 18:41:23 +08:00
Jose Olarte III
503bb1bd93 Added jdenticon to home and contact gives views 2023-07-24 19:32:30 +08:00
Jose Olarte III
9fa3b8be0b Built jdenticon component 2023-07-24 19:32:11 +08:00
3b1a9b9c5b change description since issuer isn't necessarily giver, and add a task 2023-07-23 11:35:03 -06:00
Jose Olarte III
f55e50067f Replaced all alertMessage calls with notiwind 2023-07-21 20:26:00 +08:00
7f48149d6f add some detail for the map pin, plus other refactors 2023-07-20 20:59:53 -06:00
c5b4921583 fix failure to retrieve all totals for gives from me 2023-07-20 20:33:55 -06:00
b28689ad06 walk back edits that were forward-looking but broken 2023-07-20 20:15:17 -06:00
0444b5be32 fix so that contact recipient is also recorded 2023-07-20 20:12:13 -06:00
4866416aae fix didInfo logic, and add to project lists 2023-07-20 19:02:18 -06:00
e48a4ed05b fix to use the real project name, and add creator 2023-07-20 18:35:27 -06:00
87cfead094 fix display of gives on contact screen; adjust give UI for project 2023-07-20 18:22:45 -06:00
179a5cd9f8 add task for publicly-accessible project 2023-07-20 08:32:43 -06:00
Jose Olarte III
eff67c2a4a Replaced alertMessage calls with Notiwind 2023-07-20 21:51:23 +08:00
Jose Olarte III
db22d559b7 User-friendly error messages 2023-07-20 18:04:08 +08:00
Matthew Raymer
c4443f2ed1 Added error handling and new alerts in DiscoverView 2023-07-20 16:36:33 +08:00
Jose Olarte III
05a7758c65 New notification system + test
Set of buttons added to home view for preview. Comes in toast (self-dismiss) and context alert (info, warning, danger) variants.
2023-07-19 19:48:22 +08:00
d0ec7930e1 Merge pull request 'UI improvements' (#46) from contacts-view-improvements into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#46
2023-07-19 02:18:01 -04:00
3e2cd1291c Merge branch 'master' into contacts-view-improvements 2023-07-19 02:17:55 -04:00
8d42fe905d Merge pull request 'Fixes to search bar and list top' (#45) from projects-view-improvements into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#45
2023-07-19 02:17:44 -04:00
a7e98c8f1a Merge branch 'master' into projects-view-improvements 2023-07-18 06:56:01 -04:00
f07c804b24 Merge branch 'master' into contacts-view-improvements 2023-07-18 06:55:02 -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
7c77578f79 Fixes to search bar and list top 2023-07-18 17:33:49 +08:00
Jose Olarte III
34636d6047 Fixed alert message visibility 2023-07-18 17:14:03 +08:00
Jose Olarte III
5134e2f562 UI improvements
- Consistent button styling for interactive elements
- Tooltips relegated to title attribute to avoid blocking other buttons
2023-07-18 16:40:16 +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
e42b3ff11d allow choice of no identity (for testing) 2023-07-04 13:15:52 -06:00
41 changed files with 3772 additions and 1738 deletions

990
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,45 +21,46 @@
"@veramo/did-provider-ethr": "^5.1.2", "@veramo/did-provider-ethr": "^5.1.2",
"@veramo/did-resolver": "^5.2.0", "@veramo/did-resolver": "^5.2.0",
"@veramo/key-manager": "^5.1.2", "@veramo/key-manager": "^5.1.2",
"@vueuse/core": "^10.2.0", "@vueuse/core": "^10.2.1",
"@zxing/text-encoding": "^0.9.0", "@zxing/text-encoding": "^0.9.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"core-js": "^3.31.0", "core-js": "^3.31.1",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"dexie-export-import": "^4.0.7", "dexie-export-import": "^4.0.7",
"did-jwt": "^7.2.2", "did-jwt": "^7.2.4",
"ethereum-cryptography": "^2.0.0", "ethereum-cryptography": "^2.0.0",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0", "ethr-did-resolver": "^8.0.0",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"localstorage-slim": "^2.4.0", "localstorage-slim": "^2.4.0",
"luxon": "^3.3.0", "luxon": "^3.3.0",
"merkletreejs": "^0.3.10", "merkletreejs": "^0.3.10",
"moment": "^2.29.4", "moment": "^2.29.4",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pina": "^0.20.2204228", "pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.1.0",
"qr-code-generator-vue3": "^1.4.21", "qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.0", "ramda": "^0.29.0",
"readable-stream": "^4.4.0", "readable-stream": "^4.4.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"three": "^0.153.0", "three": "^0.154.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-class-component": "^8.0.0-0",
"vue-facing-decorator": "^2.1.20", "vue-facing-decorator": "^2.1.20",
"vue-property-decorator": "^9.1.2", "vue-property-decorator": "^9.1.2",
"vue-router": "^4.2.2", "vue-router": "^4.2.3",
"web-did-resolver": "^2.0.24" "web-did-resolver": "^2.0.27"
}, },
"devDependencies": { "devDependencies": {
"@types/ramda": "^0.29.2", "@types/ramda": "^0.29.3",
"@types/three": "^0.152.1", "@types/three": "^0.152.1",
"@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.60.0", "@typescript-eslint/parser": "^5.61.0",
"@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8", "@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8", "@vue/cli-plugin-pwa": "~5.0.8",
@@ -69,13 +70,13 @@
"@vue/cli-service": "~5.0.8", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"eslint": "^8.43.0", "eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^5.0.0-alpha.1",
"eslint-plugin-vue": "^9.15.0", "eslint-plugin-vue": "^9.15.1",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"prettier": "^2.8.8", "prettier": "^3.0.0",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "~5.1.3" "typescript": "~5.1.6"
} }
} }

View File

@@ -1,71 +1,62 @@
tasks: tasks:
- replace user-affecting console.log & console.error with error messages (eg. catches) - .2 bug - on contacts view, click on "to" & "from" and nothing happens
- if there's no identity, handle it on pages which expect an identity (eg. project -- look for JSON.parse identity calls)
- .1 show an appropriate message when there are no contacts
- 8 Move to vue-facing-decorator
- 01 design ideas for simple gives on the first page
- .1 remove commitments from ProjectView UI
- 01 add list of 'give' records for a project on ProjectView UI
- 02 Discover page - display results (currently in console.log), spin when searching
- 08 search by location, endpoint, etc assignee:trent
- 01 add a location for a project via map pin : - 01 add a location for a project via map pin :
- give attribute to use assignee:trent - add with a "location" field containing this: { "geo":{ "@type":"GeoCoordinates", "latitude":40.883944, "longitude":-111.884787 } }
- 01 remove all the "form" fields (or at least investigate to see if that page refresh is desired) - 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. - 08 Scan QR code to import into contacts.
- contacts v1 : - 40 notifications :
- 01 Import contact info a la QR code. - push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data
- 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 move all "identity" references to temporary account access assignee:trent
- contacts v+ :
- .2 show error to user when adding a duplicate contact
- 01 parse input more robustly (with CSV lib and not commas)
- refactor UI : - refactor UI :
- .5 Alerts show at the top and can be missed, eg. account data download - .5 Alerts show at the top and can be missed if you've scrolled down on the page, eg. account data download
- 01 Change alert popup code a component (to cut down duplicate code; see "in many files") - .2 Make alerts at the top more visible (because they're currently a similar color and sometimes aren't seen)
- 01 Change "nav" tabs across the bottom into a component (eliminating duplicate code).
- .5 Fix how icons show on top of bottom bar on ContactAmounts page
- .2 Hide "Advanced" section in Account page by default
- show pop-up confirming that settings & contacts have been downloaded - 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 - registration - Ensure each action sent to the server has a confirmation - eg registration
- Home Feed & Quick Give screen : - Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery") - 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 - 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
- .5 customize favicon
- .2 Hide "Advanced" section in Account page by default
- 04 allow user to download claims, mine + ones I can see about me from others
- 24 Move to Vite - 24 Move to Vite
- 40 notifications : - .5 add link to further project / people when a project pays ahead
- push - .5 add project ID to the URL, to make a project publicly-accessible
- .5 remove edit from project page for projects owned by others
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
- .2 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
- .2 move 'switch identity' to the advanced section
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
- Discuss whether the remaining tasks are worthwhile before MVP release. - Discuss whether the remaining tasks are worthwhile before MVP release.
- 01 fix images on project page, on discovery page - contacts v+ :
- .2 fix "Rotary" and static icon to the right on project page - 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 : - stats v1 :
- 01 show numeric stats - 01 show numeric stats
- 01 link to world for specific stats - 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists - .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") - maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- 4-8 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie) - 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- Do we want split first name & last name? - .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages
- remove 'about' page - .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 : - Release Minimum Viable Product :
- 08 thorough testing for errors & edge cases - 08 thorough testing for errors & edge cases
@@ -78,13 +69,11 @@ tasks:
- Test PWA features on Android and iOS. - Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- 40 notifications v+ :
- pull, w/ scheduled runs
- linking between projects or plans : - linking between projects or plans :
- terminology: - show total time given to & from a project
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances - terminology:
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning) - for 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 : - Stats :
- 01 point out user's location on the world - 01 point out user's location on the world
@@ -104,11 +93,14 @@ tasks:
- Peer DID - Peer DID
- DIDComm - DIDComm
- Write to or read from a different ledger (eg. private ACDC, attest.sh) - 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: log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29 - videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29

View File

@@ -1,5 +1,132 @@
<template> <template>
<router-view /> <router-view />
<NotificationGroup group="alert">
<div
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
>
<Notification
v-slot="{ notifications, close }"
enter="transform ease-out duration-300 transition"
enter-from="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4"
enter-to="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-500"
leave-from="opacity-100"
leave-to="opacity-0"
move="transition duration-500"
move-delay="delay-300"
>
<div
v-for="notification in notifications"
:key="notification.id"
class="w-full"
role="alert"
>
<div
v-if="notification.type === 'toast'"
class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md"
>
<div class="w-full px-4 py-3">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ notification.text }}</p>
</div>
</div>
<div
v-if="notification.type === 'info'"
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-100 rounded-lg shadow-md"
>
<div
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
>
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ notification.text }}</p>
<button
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
>
<fa icon="xmark" class="fa-fw"></fa>
</button>
</div>
</div>
<div
v-if="notification.type === 'success'"
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-emerald-100 rounded-lg shadow-md"
>
<div
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
>
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ notification.text }}</p>
<button
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
>
<fa icon="xmark" class="fa-fw"></fa>
</button>
</div>
</div>
<div
v-if="notification.type === 'warning'"
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-amber-100 rounded-lg shadow-md"
>
<div
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
>
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ notification.text }}</p>
<button
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
>
<fa icon="xmark" class="fa-fw"></fa>
</button>
</div>
</div>
<div
v-if="notification.type === 'danger'"
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-rose-100 rounded-lg shadow-md"
>
<div
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
>
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ notification.text }}</p>
<button
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
>
<fa icon="xmark" class="fa-fw"></fa>
</button>
</div>
</div>
</div>
</Notification>
</div>
</NotificationGroup>
</template> </template>
<style></style> <style></style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-bind:class="computedAlertClassNames()"> <div v-bind:class="computedAlertClassNames()">
<button <button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2" class="close-button bg-amber-400 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()" @click="onClickClose()"
> >
<fa icon="xmark"></fa> <fa icon="xmark"></fa>
@@ -28,7 +28,7 @@ export default class AlertMessage extends Vue {
return { return {
hidden: !this.isAlertVisible, hidden: !this.isAlertVisible,
"dismissable-alert": true, "dismissable-alert": true,
"bg-slate-100": true, "bg-amber-200": true,
"p-5": true, "p-5": true,
rounded: true, rounded: true,
"drop-shadow-lg": true, "drop-shadow-lg": true,

View File

@@ -0,0 +1,19 @@
<template>
<div v-html="generateIdenticon()" class="w-fit"></div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { toSvg } from "jdenticon";
@Component
export default class EntityIcon extends Vue {
@Prop entityId = "";
@Prop iconSize = "";
generateIdenticon() {
const svgString = toSvg(this.entityId, this.iconSize);
return svgString;
}
}
</script>
<style scoped></style>

View File

@@ -1,41 +1,51 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-lg text-center"> <h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giver?.name || "somebody not specified" }} {{ message }} {{ giver?.name || "somebody not specified" }}
</h1> </h1>
<input <input
type="text" type="text"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received" placeholder="What was received"
v-model="description" v-model="description"
/> />
<div class="flex flex-row"> <div class="flex flex-row mb-6">
<span class="py-4">Hours</span> <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 <input
type="text" type="text"
class="block w-8 rounded border border-slate-400 ml-4 text-center" class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="hours" v-model="hours"
/> />
<div class="flex flex-col px-1"> <div
<div> class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
<fa icon="square-caret-up" size="2xl" @click="increment()" /> @click="increment()"
</div> >
<div> <fa icon="chevron-right" />
<fa icon="square-caret-down" size="2xl" @click="decrement()" />
</div>
</div> </div>
</div> </div>
<p class="text-right">Sign & Send to publish to the world</p> <p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
<div class="text-right"> <button
<button class="rounded border border-slate-400" @click="confirm"> class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
<span class="m-2">Sign & Send</span> @click="confirm"
</button> >
&nbsp; Sign &amp; Send
<button class="rounded border border-slate-400" @click="cancel"> </button>
<span class="m-2">Cancel</span> <button
</button> class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
</div> @click="cancel"
>
Cancel
</button>
</div> </div>
</div> </div>
</template> </template>
@@ -106,12 +116,14 @@ export default class GiftedDialog extends Vue {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 1.5rem;
} }
.dialog { .dialog {
background-color: white; background-color: white;
padding: 1rem; padding: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
width: 50%; width: 100%;
max-width: 500px;
} }
</style> </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

@@ -24,7 +24,7 @@ export default class InfiniteScroll extends Vue {
}; };
this.observer = new IntersectionObserver( this.observer = new IntersectionObserver(
this.handleIntersection, this.handleIntersection,
options options,
); );
this.observer.observe(this.$refs.sentinel as HTMLElement); this.observer.observe(this.$refs.sentinel as HTMLElement);
} }

View File

@@ -5,7 +5,7 @@ function createCamera() {
35, // fov = Field Of View 35, // fov = Field Of View
1, // aspect ratio (dummy value) 1, // aspect ratio (dummy value)
0.1, // near clipping plane 0.1, // near clipping plane
350 // far clipping plane 350, // far clipping plane
); );
// move the camera back so we can view the scene // move the camera back so we can view the scene

View File

@@ -22,17 +22,16 @@ export async function loadLandmarks(vue, world, scene, loop) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token,
}; };
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 }); const resp = await axios.get(url, { headers: headers });
if (resp.status === 200) { if (resp.status === 200) {
const landmarks = resp.data.data; const landmarks = resp.data.data;
@@ -63,7 +62,11 @@ export async function loadLandmarks(vue, world, scene, loop) {
// calculate positions for each claim, especially because some are random // calculate positions for each claim, especially because some are random
const locations = landmarks.map((claim) => const locations = landmarks.map((claim) =>
locForGive(claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS) locForGive(
claim,
world.PLATFORM_SIZE,
world.PLATFORM_EDGE_FOR_UNKNOWNS,
),
); );
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -93,7 +96,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
undefined, undefined,
function (error) { function (error) {
console.error(error); console.error(error);
} },
); );
// calculate when lights shine on appearing claim area // calculate when lights shine on appearing claim area
@@ -121,7 +124,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
.onComplete(() => { .onComplete(() => {
scene.remove(light); scene.remove(light);
light.dispose(); light.dispose();
}) }),
) )
.start(); .start();
world.lights = [...world.lights, light]; world.lights = [...world.lights, light];
@@ -130,18 +133,18 @@ export async function loadLandmarks(vue, world, scene, loop) {
console.error( console.error(
"Got bad server response status & data of", "Got bad server response status & data of",
resp.status, resp.status,
resp.data resp.data,
); );
vue.setAlert( vue.setAlert(
"Error With Server", "Error With Server",
"There was an error retrieving your claims from the server." "There was an error retrieving your claims from the server.",
); );
} }
} catch (error) { } catch (error) {
console.error("Got exception contacting server:", error); console.error("Got exception contacting server:", error);
vue.setAlert( vue.setAlert(
"Error With Server", "Error With Server",
"There was a problem retrieving your claims from the server." "There was a problem retrieving your claims from the server.",
); );
} }
} }

View File

@@ -4,7 +4,7 @@
export enum AppString { export enum AppString {
APP_NAME = "Kick-Start with Time", APP_NAME = "Kick-Start with Time",
PROD_ENDORSER_API_SERVER = "https://endorser.ch:3000", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000", TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000", LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",

View File

@@ -54,7 +54,6 @@ if (localStorage.getItem("secret") == null) {
localStorage.setItem("secret", secret); localStorage.setItem("secret", secret);
} }
//console.log("IndexedDB Encryption Secret:", secret);
encrypted(accountsDB, { secretKey: secret }); encrypted(accountsDB, { secretKey: secret });
accountsDB.version(1).stores(SensitiveSchemas); accountsDB.version(1).stores(SensitiveSchemas);

View File

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

View File

@@ -3,6 +3,7 @@ import { IIdentifier } from "@veramo/core";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios"; import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
export const SERVICE_ID = "endorser.ch"; export const SERVICE_ID = "endorser.ch";
@@ -81,12 +82,17 @@ export function isHiddenDid(did) {
/** /**
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
**/ **/
export function didInfo(did, identifiers, contacts) { export function didInfo(
const myId = R.find((i) => i.did === did, identifiers); did: string,
activeDid: string,
allMyDids: Array<string>,
contacts: Array<Contact>,
): string {
const myId: string | undefined = R.find(R.equals(did), allMyDids, did);
if (myId) { if (myId) {
return "You"; return "You" + (myId !== activeDid ? " (Alt ID)" : "");
} else { } else {
const contact = R.find((c) => c.did === did, contacts); const contact: Contact | undefined = R.find((c) => c.did === did, contacts);
if (contact) { if (contact) {
return contact.name || "Someone Unnamed in Contacts"; return contact.name || "Someone Unnamed in Contacts";
} else if (!did) { } else if (!did) {
@@ -100,7 +106,7 @@ export function didInfo(did, identifiers, contacts) {
} }
/** /**
* For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
* *
* @param identity * @param identity
* @param fromDid may be null * @param fromDid may be null
@@ -116,7 +122,7 @@ export async function createAndSubmitGive(
toDid: string, toDid: string,
description: string, description: string,
hours: number, hours: number,
fulfillsProjectHandleId?: string fulfillsProjectHandleId?: string,
): Promise<AxiosResponse<ClaimResult> | InternalError> { ): Promise<AxiosResponse<ClaimResult> | InternalError> {
// Make a claim // Make a claim
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
@@ -193,3 +199,53 @@ export function isNumeric(str: string): boolean {
export function numberOrZero(str: string): number { export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0; return isNumeric(str) ? +str : 0;
} }
export interface ErrorResponse {
error?: {
message?: string;
};
}
export interface RateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
/**
* Represents data about a project
**/
export interface ProjectData {
/**
* Name of the project
**/
name: string;
/**
* Description of the project
**/
description: string;
/**
* URL referencing information about the project
**/
handleId: string;
/**
* The 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

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

View File

@@ -5,6 +5,7 @@ import "./registerServiceWorker";
import router from "./router"; import router from "./router";
import axios from "axios"; import axios from "axios";
import VueAxios from "vue-axios"; import VueAxios from "vue-axios";
import Notifications from "notiwind";
import "./assets/styles/tailwind.css"; import "./assets/styles/tailwind.css";
@@ -13,12 +14,15 @@ import {
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
faChevronRight,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo,
faCircleQuestion, faCircleQuestion,
faCircleUser, faCircleUser,
faClock, faClock,
faCoins, faCoins,
faComment,
faCopy, faCopy,
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
@@ -43,6 +47,7 @@ import {
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faTrashCan, faTrashCan,
faTriangleExclamation,
faUser, faUser,
faUsers, faUsers,
faXmark, faXmark,
@@ -52,12 +57,15 @@ library.add(
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
faChevronRight,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo,
faCircleQuestion, faCircleQuestion,
faCircleUser, faCircleUser,
faClock, faClock,
faCoins, faCoins,
faComment,
faCopy, faCopy,
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
@@ -82,9 +90,10 @@ library.add(
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faTrashCan, faTrashCan,
faTriangleExclamation,
faUser, faUser,
faUsers, faUsers,
faXmark faXmark,
); );
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@@ -94,4 +103,5 @@ createApp(App)
.use(createPinia()) .use(createPinia())
.use(VueAxios, axios) .use(VueAxios, axios)
.use(router) .use(router)
.use(Notifications)
.mount("#app"); .mount("#app");

View File

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

View File

@@ -25,12 +25,6 @@ const routes: Array<RouteRecordRaw> = [
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"), import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: enterOrStart, beforeEnter: enterOrStart,
}, },
{
path: "/about",
name: "about",
component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
},
{ {
path: "/account", path: "/account",
name: "account", name: "account",
@@ -129,6 +123,14 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue" /* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
), ),
}, },
{
path: "/identity-switcher",
name: "identity-switcher",
component: () =>
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
},
{ {
path: "/project", path: "/project",
name: "project", name: "project",
@@ -164,6 +166,14 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue" /* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
), ),
}, },
{
path: "/contact-gives",
name: "contact-gives",
component: () =>
import(
/* webpackChunkName: "statistics" */ "../views/ContactGiftingView.vue"
),
},
]; ];
/** @type {*} */ /** @type {*} */
@@ -172,4 +182,14 @@ const router = createRouter({
routes, routes,
}); });
const errorHandler = (error, to, from) => {
// Handle the error here
console.error(error, to, from);
console.log("XXXXX");
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
};
router.onError(errorHandler); // Assign the error handler to the router instance
export default router; export default router;

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

@@ -3,10 +3,23 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
Your Identity Your Identity
</h1> </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"> <div class="flex justify-between py-2">
<span /> <span />
<span> <span>
@@ -53,14 +66,6 @@
<fa icon="copy" class="text-slate-400 fa-fw"></fa> <fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button> </button>
<span v-show="showDidCopy">Copied!</span> <span v-show="showDidCopy">Copied!</span>
<span class="whitespace-nowrap ml-4">
<router-link
:to="{ name: 'contact-qr' }"
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>
</router-link>
</span>
</div> </div>
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div> <div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
@@ -108,10 +113,16 @@
<router-link <router-link
:to="{ name: 'new-edit-account' }" :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 Edit Identity
</router-link> </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> <h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
@@ -132,133 +143,131 @@
<!-- QR code popup --> <!-- QR code popup -->
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md"> <dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
<form method="dialog"> <div class="text-slate-500 text-center">
<div class="text-slate-500 text-center"> <b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code> </div>
</div> <img src="/img/sample-qr-code.png" class="w-full mb-3" />
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
<button <button
value="cancel" 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" 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 Copy to Clipboard
</button> </button>
<button <button
value="cancel" value="cancel"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
> >
Close Close
</button> </button>
</form>
</dialog> </dialog>
<h3 class="text-sm uppercase font-semibold mb-3">Advanced</h3> <h3
class="text-sm uppercase font-semibold mb-3"
<label @click="showAdvanced = !showAdvanced"
for="toggleShowAmounts"
class="flex items-center cursor-pointer mb-6"
@click="handleChange"
> >
<!-- toggle --> Advanced
<div class="relative"> </h3>
<!-- input --> <div v-if="showAdvanced">
<input <label
type="checkbox" for="toggleShowAmounts"
v-model="showContactGives" class="flex items-center cursor-pointer mb-6"
name="showContactGives" @click="handleChange"
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> <!-- toggle -->
</button> <div class="relative">
<button <!-- input -->
class="px-4 rounded bg-slate-200 border border-slate-400" <input
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)" type="checkbox"
> v-model="showContactGives"
Use Prod name="showContactGives"
</button> class="sr-only"
<button />
class="px-4 rounded bg-slate-200 border border-slate-400" <!-- line -->
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)" <div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
> <!-- dot -->
Use Test <div
</button> class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
<button ></div>
class="px-4 rounded bg-slate-200 border border-slate-400" </div>
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)" <!-- label -->
> <div class="ml-2">Show amounts given with contacts</div>
Use Local </label>
</button>
</div>
<div v-if="numAccounts > 0" class="flex py-2"> <div class="flex py-2">
Switch Identifier <button
<span v-for="accountNum in numAccounts" :key="accountNum"> class="text-center text-md text-blue-500"
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)"> @click="checkLimits()"
#{{ accountNum }}
</button>
</span>
</div>
<div>
<button class="text-blue-500">
<router-link
:to="{ name: 'statistics' }"
class="block text-center py-3"
> >
See Achievements & Statistics Check Limits
</router-link> </button>
</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> </div>
<AlertMessage <AlertMessage
:alertTitle="alertTitle" :alertTitle="alertTitle"
@@ -269,7 +278,6 @@
<script lang="ts"> <script lang="ts">
import "dexie-export-import"; import "dexie-export-import";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
@@ -280,19 +288,11 @@ import { accessToken } from "@/libs/crypto";
import { AxiosError } from "axios/index"; import { AxiosError } from "axios/index";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav";
import { IIdentifier } from "@veramo/core";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
interface RateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
@Component({ components: { AlertMessage, QuickNav } }) @Component({ components: { AlertMessage, QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
Constants = AppString; Constants = AppString;
@@ -316,6 +316,29 @@ export default class AccountViewView extends Vue {
showB64Copy = false; showB64Copy = false;
showPubCopy = 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 // call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text, fn) { doCopyTwoSecRedo(text, fn) {
fn(); fn();
@@ -333,7 +356,11 @@ export default class AccountViewView extends Vue {
return timeStr.substring(0, timeStr.indexOf("T")); 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() { async created() {
// Uncomment this 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/ // To manage within the vue devtools browser extension https://devtools.vuejs.org/
@@ -351,27 +378,42 @@ export default class AccountViewView extends Vue {
this.lastName = settings?.lastName || ""; this.lastName = settings?.lastName || "";
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray();
this.numAccounts = accounts.length;
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
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, { if (identity) {
activeDid: identity.did, this.publicHex = identity.keys[0].publicKeyHex;
}); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString(
this.checkLimits(); "base64",
);
this.derivationPath = identity.keys[0].meta.derivationPath;
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
this.checkLimitsFor(identity);
}
} catch (err) { } catch (err) {
this.alertMessage = if (
"Clear your cache and start over (after data backup)."; err.message ===
console.error("Telling user to clear cache at page create because:", err); "Attempted to load account records with no identity available."
this.alertTitle = "Error Creating Account"; ) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Creating Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error(
"Telling user to clear cache at page create because:",
err,
);
}
} }
} }
@@ -382,13 +424,19 @@ export default class AccountViewView extends Vue {
showContactGivesInline: this.showContactGives, showContactGivesInline: this.showContactGives,
}); });
} catch (err) { } catch (err) {
this.alertMessage = this.$notify(
"Clear your cache and start over (after data backup)."; {
group: "alert",
type: "danger",
title: "Error Updating Contact Setting",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error( console.error(
"Telling user to clear cache after contact setting update because:", "Telling user to clear cache after contact setting update because:",
err err,
); );
this.alertTitle = "Error Updating Contact Setting";
} }
} }
@@ -404,66 +452,99 @@ export default class AccountViewView extends Vue {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
this.alertTitle = "Download Started"; this.$notify(
this.alertMessage = "See your downloads directory for the backup."; {
group: "alert",
type: "toast",
title: "Download Started",
text: "See your downloads directory for the backup.",
},
5000,
);
} catch (error) { } catch (error) {
this.alertTitle = "Export Error"; this.$notify(
this.alertMessage = "See console logs for more info."; {
group: "alert",
type: "danger",
title: "Export Error",
text: "See console logs for more info.",
},
-1,
);
console.error("Export Error:", error); console.error("Export Error:", error);
} }
} }
async checkLimits() { async checkLimits() {
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.checkLimitsFor(identity);
}
}
async checkLimitsFor(identity: IIdentifier) {
this.loadingLimits = true; this.loadingLimits = true;
this.limitsMessage = ""; this.limitsMessage = "";
const url = this.apiServer + "/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 || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const url = this.apiServer + "/api/report/rateLimits";
const headers = await this.getHeaders(identity);
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
// axios throws an exception on a 400 // axios throws an exception on a 400
if (resp.status === 200) { if (resp.status === 200) {
this.limits = resp.data; this.limits = resp.data;
} }
} catch (error: unknown) { } catch (error: unknown) {
const serverError = error as AxiosError; if (
error.message ===
"Attempted to load Give records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
const serverError = error as AxiosError;
console.error("Bad response retrieving limits: ", serverError);
console.error("Bad response retrieving limits: ", serverError); const data: ErrorResponse | undefined =
// Anybody know how to access items inside "response.data" without this? serverError.response && serverError.response.data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (data && data.error && data.error.message) {
const data: any = serverError.response?.data; this.limitsMessage = data.error.message;
this.limitsMessage = data?.error?.message || "Bad server response."; } else {
this.limitsMessage = "Bad server response.";
}
}
} }
this.loadingLimits = false; this.loadingLimits = false;
} }
async switchAccount(accountNum: number) { async switchAccount(accountNum: number) {
await accountsDB.open(); // 0 means none
const accounts = await accountsDB.accounts.toArray(); if (accountNum === 0) {
const account = accounts[accountNum - 1]; 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(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did, activeDid: account.did,
}); });
this.activeDid = account.did; this.activeDid = account.did;
this.derivationPath = account.derivationPath; this.derivationPath = account.derivationPath;
this.publicHex = account.publicKeyHex; this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
}
} }
public showContactGivesClassNames() { public showContactGivesClassNames() {
@@ -484,9 +565,5 @@ export default class AccountViewView extends Vue {
setApiServerInput(value) { setApiServerInput(value) {
this.apiServerInput = value; this.apiServerInput = value;
} }
// This same popup code is in many files.
alertMessage = "";
alertTitle = "";
} }
</script> </script>

View File

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

View File

@@ -1,52 +1,14 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Contacts"></QuickNav>
<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>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Given with {{ contact?.name }} Given with {{ contact?.name }}
</h1> </h1>
<div class="flex justify-around">
<span />
<span class="justify-around">(Only 50 most recent)</span>
<span />
</div>
<!-- Results List --> <!-- Results List -->
<div> <div>
@@ -133,8 +95,9 @@ import {
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage } }) @Component({ components: { AlertMessage, QuickNav } })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
@@ -142,8 +105,38 @@ export default class ContactsView extends Vue {
giveRecords: Array<GiveServerRecord> = []; giveRecords: Array<GiveServerRecord> = [];
alertTitle = ""; alertTitle = "";
alertMessage = ""; 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() { async created() {
try { try {
await db.open(); await db.open();
@@ -158,38 +151,31 @@ export default class ContactsView extends Vue {
this.loadGives(this.activeDid, this.contact); this.loadGives(this.activeDid, this.contact);
} }
} catch (err) { } catch (err) {
this.alertTitle = "Error"; this.$notify(
this.alertMessage = {
err.userMessage || group: "alert",
"There was an error retrieving the latest sweet, sweet action."; type: "danger",
title: "Error",
text:
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.",
},
-1,
);
} }
} }
async loadGives(activeDid: string, contact: Contact) { 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 || "null");
if (!identity) {
throw new Error("No identity found.");
}
// load all the time I have given to them
try { try {
const identity = await this.getIdentity(this.activeDid);
let result = []; let result = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did) + encodeURIComponent(identity.did) +
"&recipientDid=" + "&recipientDid=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const token = await accessToken(identity); const headers = await this.getHeaders(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200) { if (resp.status === 200) {
result = resp.data.data; result = resp.data.data;
@@ -197,11 +183,17 @@ export default class ContactsView extends Vue {
console.error( console.error(
"Got bad response status & data of", "Got bad response status & data of",
resp.status, resp.status,
resp.data resp.data,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: "Got an error retrieving your given time from the server.",
},
-1,
); );
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
} }
const url2 = const url2 =
@@ -210,11 +202,7 @@ export default class ContactsView extends Vue {
encodeURIComponent(contact.did) + encodeURIComponent(contact.did) +
"&recipientDid=" + "&recipientDid=" +
encodeURIComponent(identity.did); encodeURIComponent(identity.did);
const token2 = await accessToken(identity); const headers2 = await this.getHeaders(identity);
const headers2 = {
"Content-Type": "application/json",
Authorization: "Bearer " + token2,
};
const resp2 = await this.axios.get(url2, { headers: headers2 }); const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) { if (resp2.status === 200) {
result = R.concat(result, resp2.data.data); result = R.concat(result, resp2.data.data);
@@ -222,22 +210,35 @@ export default class ContactsView extends Vue {
console.error( console.error(
"Got bad response status & data of", "Got bad response status & data of",
resp2.status, resp2.status,
resp2.data resp2.data,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: "Got an error retrieving your given time from the server.",
},
-1,
); );
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
} }
const sortedResult: Array<GiveServerRecord> = R.sort( const sortedResult: Array<GiveServerRecord> = R.sort(
(a, b) => (a, b) =>
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(), new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
result result,
); );
this.giveRecords = sortedResult; this.giveRecords = sortedResult;
} catch (error) { } catch (error) {
this.alertTitle = "Error With Server"; this.$notify(
this.alertMessage = error as string; {
group: "alert",
type: "danger",
title: "Error With Server",
text: error as string,
},
-1,
);
} }
} }
@@ -266,13 +267,7 @@ export default class ContactsView extends Vue {
}; };
// Create a signature using private key of identity // Create a signature using private key of identity
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
if (identity.keys[0].privateKeyHex !== null) { if (identity.keys[0].privateKeyHex !== null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!; const privateKeyHex: string = identity.keys[0].privateKeyHex!;
@@ -296,7 +291,6 @@ export default class ContactsView extends Vue {
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success) { if (resp.data?.success) {
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1; record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
} }
@@ -313,15 +307,29 @@ export default class ContactsView extends Vue {
userMessage = error as string; userMessage = error as string;
} }
// Now set that error for the user to see. // Now set that error for the user to see.
this.alertTitle = "Error With Server"; this.$notify(
this.alertMessage = userMessage; {
group: "alert",
type: "danger",
title: "Error With Server",
text: userMessage,
},
-1,
);
} }
} }
} }
cannotConfirmMessage() { cannotConfirmMessage() {
this.alertTitle = "Not Allowed"; this.$notify(
this.alertMessage = "Only the recipient can confirm final receipt."; {
group: "alert",
type: "danger",
title: "Not Allowed",
text: "Only the recipient can confirm final receipt.",
},
-1,
);
} }
} }
</script> </script>

View File

@@ -0,0 +1,311 @@
<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 text-slate-500"
><EntityIcon
entityId="Anonymous"
:iconSize="32"
class="opacity-50 inline-block align-middle border border-dashed border-slate-400 bg-slate-200 rounded-md mr-1"
></EntityIcon>
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"
><EntityIcon
:entityId="contact.did"
:iconSize="32"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
></EntityIcon>
{{ 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";
import EntityIcon from "@/components/EntityIcon";
@Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
})
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.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.",
},
-1,
);
}
}
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.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record a give.",
},
-1,
);
return;
}
if (!description && !hours) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
},
-1,
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await 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.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error recording the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That gift was recorded.",
},
-1,
);
}
} catch (error) {
console.log("Error with give caught:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
getGiveErrorMessage(error) ||
"There was an error recording the give.",
},
-1,
);
}
}
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

@@ -37,6 +37,8 @@ import QuickNav from "@/components/QuickNav";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
alertTitle = "";
alertMessage = "";
@Component({ @Component({
components: { components: {
@@ -50,6 +52,29 @@ export default class ContactQRScanShow extends Vue {
apiServer = ""; apiServer = "";
qrValue = ""; 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() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -60,13 +85,17 @@ export default class ContactQRScanShow extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (!account) { if (!account) {
this.alertMessage = "You have no identity yet."; this.$notify(
{
group: "alert",
type: "warning",
title: "",
text: "You have no identity yet.",
},
-1,
);
} else { } else {
const identity = JSON.parse(account?.identity || "null"); const identity = await this.getIdentity(this.activeDid);
if (!identity) {
throw new Error("No identity found.");
}
const publicKeyHex = identity.keys[0].publicKeyHex; const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const contactInfo = { const contactInfo = {
@@ -91,9 +120,5 @@ export default class ContactQRScanShow extends Vue {
this.qrValue = viewPrefix + vcJwt; this.qrValue = viewPrefix + vcJwt;
} }
} }
// This same popup code is in many files.
alertTitle = "";
alertMessage = "";
} }
</script> </script>

View File

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

View File

@@ -66,18 +66,27 @@
: "Unconfirmed" : "Unconfirmed"
}} }}
</button> </button>
<br />
(Only hours shown)
<br />
(Only recent shown)
</div> </div>
</div> </div>
<!-- Results List --> <!-- Results List -->
<ul class=""> <ul v-if="contacts.length > 0" class="border-t border-slate-300">
<li <li
class="border-b border-slate-300" class="border-b border-slate-300 pt-2.5 pb-4"
v-for="contact in contacts" v-for="contact in contacts"
:key="contact.did" :key="contact.did"
> >
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<h2 class="text-base font-semibold"> <h2 class="text-base font-semibold">
<EntityIcon
:entityId="contact.did"
:iconSize="24"
class="inline-block align-text-bottom border border-slate-300 rounded"
></EntityIcon>
{{ contact.name || "(no name)" }} {{ contact.name || "(no name)" }}
</h2> </h2>
<div class="text-sm truncate">{{ contact.did }}</div> <div class="text-sm truncate">{{ contact.did }}</div>
@@ -85,70 +94,85 @@
Public Key (base 64): {{ contact.publicKeyBase64 }} Public Key (base 64): {{ contact.publicKeyBase64 }}
</div> </div>
<button <div id="ContactActions" class="flex gap-1.5 mt-2">
v-if="contact.seesMe" <button
class="tooltip" v-if="contact.seesMe"
@click="setVisibility(contact, false)" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
> @click="setVisibility(contact, false)"
<fa icon="eye" class="text-slate-900 fa-fw ml-1" /> title="They can see you"
<span class="tooltiptext">They can see you</span> >
</button> <fa icon="eye" class="fa-fw" />
<button v-else class="tooltip" @click="setVisibility(contact, true)"> </button>
<span class="tooltiptext">They cannot see you</span> <button
<fa icon="eye-slash" class="text-slate-900 fa-fw ml-1" /> v-else
</button> class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true)"
title="They cannot see you"
>
<fa icon="eye-slash" class="fa-fw" />
</button>
<button class="tooltip" @click="checkVisibility(contact)"> <button
<span class="tooltiptext">Check Visibility</span> class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
<fa icon="rotate" class="text-slate-900 fa-fw ml-1" /> @click="checkVisibility(contact)"
</button> title="Check Visibility"
>
<fa icon="rotate" class="fa-fw" />
</button>
<button v-if="contact.registered" class="tooltip"> <button
<span class="tooltiptext">Registered</span> v-if="contact.registered"
<fa icon="person-circle-check" class="text-slate-900 fa-fw ml-1" /> class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
</button> title="Registered"
<button v-else @click="register(contact)" class="tooltip"> >
<span class="tooltiptext">Registration Unknown</span> <fa icon="person-circle-check" class="fa-fw" />
<fa </button>
icon="person-circle-question" <button
class="text-slate-900 fa-fw ml-1" v-else
/> @click="register(contact)"
</button> class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="Registration unknown"
>
<fa icon="person-circle-question" class="fa-fw" />
</button>
<button @click="deleteContact(contact)" class="px-9 tooltip"> <button
<span class="tooltiptext">Delete!</span> @click="deleteContact(contact)"
<fa icon="trash-can" class="text-red-600 fa-fw ml-1" /> class="text-sm uppercase bg-red-600 text-white px-2 py-1.5 rounded-md"
</button> title="Delete"
>
<fa icon="trash-can" class="fa-fw" />
</button>
<div v-if="showGiveNumbers" class="float-right"> <div
<div class="float-right"> v-if="showGiveNumbers && contact.did != activeDid"
<div class="tooltip"> class="ml-auto flex gap-1.5"
to: >
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
@click="onClickAddGive(activeDid, contact.did)"
title="givenByMeDescriptions[contact.did]"
>
To:
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
this.showGiveTotals this.showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0) ? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0)) + (givenByMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed : this.showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0) ? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0) : (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
<span <fa icon="plus" />
v-if="givenByMeDescriptions[contact.did]" </button>
class="tooltiptext-left"
> <button
{{ givenByMeDescriptions[contact.did] }} class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
</span> @click="onClickAddGive(contact.did, activeDid)"
<button title="givenToMeDescriptions[contact.did]"
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" >
@click="onClickAddGive(activeDid, contact.did)" From:
>
+
</button>
</div>
<div class="tooltip px-2">
from:
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
this.showGiveTotals this.showGiveTotals
@@ -159,34 +183,25 @@
: (givenToMeUnconfirmed[contact.did] || 0) : (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
<span <fa icon="plus" />
v-if="givenToMeDescriptions[contact.did]" </button>
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"
@click="onClickAddGive(contact.did, activeDid)"
>
+
</button>
</div>
<router-link <router-link
:to="{ :to="{
name: 'contact-amounts', name: 'contact-amounts',
query: { contactDid: contact.did }, query: { contactDid: contact.did },
}" }"
class="tooltip" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="See all given activity"
> >
<fa icon="file-lines" class="text-slate-600 fa-fw ml-1" /> <fa icon="file-lines" class="fa-fw" />
<span class="tooltiptext-left">See All Given Activity</span>
</router-link> </router-link>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
</ul> </ul>
<p v-else>This identity has no contacts.</p>
<AlertMessage <AlertMessage
:alertTitle="alertTitle" :alertTitle="alertTitle"
:alertMessage="alertMessage" :alertMessage="alertMessage"
@@ -199,26 +214,25 @@ import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { import {
GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
RegisterVerifiableCredential, RegisterVerifiableCredential,
SERVICE_ID, SERVICE_ID,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Component, Vue } from "vue-facing-decorator";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@Component({ @Component({
components: { AlertMessage, QuickNav }, components: { AlertMessage, QuickNav, EntityIcon },
}) })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
activeDid = ""; activeDid = "";
@@ -242,8 +256,9 @@ export default class ContactsView extends Vue {
showGiveNumbers = false; showGiveNumbers = false;
showGiveTotals = true; showGiveTotals = true;
showGiveConfirmed = true; showGiveConfirmed = true;
alertTitle = "";
alertMessage = "";
// 'created' hook runs when the Vue instance is first created
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -257,126 +272,140 @@ export default class ContactsView extends Vue {
const allContacts = await db.contacts.toArray(); const allContacts = await db.contacts.toArray();
this.contacts = R.sort( this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts allContacts,
); );
} }
async loadGives() { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
console.error( throw new Error(
"Attempted to load Give records with no identity available." "Attempted to load Give records with no identity available.",
); );
return;
} }
return identity;
}
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,
useRecipient,
) => {
if (resp.status === 200) {
const allData = resp.data.data;
for (const give of allData) {
const otherDid = useRecipient ? give.recipientDid : give.agentDid;
if (give.unit === "HUR") {
if (give.amountConfirmed) {
const prevAmount = confirmed[otherDid] || 0;
confirmed[otherDid] = prevAmount + give.amount;
} else {
const prevAmount = unconfirmed[otherDid] || 0;
unconfirmed[otherDid] = prevAmount + give.amount;
}
if (!descriptions[otherDid] && give.description) {
descriptions[otherDid] = give.description;
}
}
}
} else {
console.error(
"Got bad response status & data of",
resp.status,
resp.data,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text:
"Got an error retrieving your " +
resp.config.url.includes("recipientDid")
? "received"
: "given" + " time from the server.",
},
-1,
);
}
};
// load all the time I have given
try { try {
const url = const { headers } = await this.getHeadersAndIdentity(this.activeDid);
const givenByUrl =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did); encodeURIComponent(this.activeDid);
const token = await accessToken(identity); const givenToUrl =
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.error(
"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.";
}
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
}
// load all the time I have received
try {
const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?recipientDid=" + "/api/v2/report/gives?recipientDid=" +
encodeURIComponent(identity.did); encodeURIComponent(this.activeDid);
const token = await accessToken(identity);
const headers = { const [givenByMeResp, givenToMeResp] = await Promise.all([
"Content-Type": "application/json", this.axios.get(givenByUrl, { headers }),
Authorization: "Bearer " + token, this.axios.get(givenToUrl, { headers }),
}; ]);
const resp = await this.axios.get(url, { headers });
//console.log("All gifts you've recieved:", resp.data); const givenByMeDescriptions = {};
if (resp.status === 200) { const givenByMeConfirmed = {};
const contactDescriptions: Record<string, string> = {}; const givenByMeUnconfirmed = {};
const contactConfirmed: Record<string, number> = {}; handleResponse(
const contactUnconfirmed: Record<string, number> = {}; givenByMeResp,
const allData: Array<GiveServerRecord> = resp.data.data; givenByMeDescriptions,
for (const give of allData) { givenByMeConfirmed,
if (give.unit == "HUR") { givenByMeUnconfirmed,
if (give.amountConfirmed) { true,
const prevAmount = contactConfirmed[give.agentDid] || 0; );
contactConfirmed[give.agentDid] = prevAmount + give.amount; this.givenByMeDescriptions = givenByMeDescriptions;
} else { this.givenByMeConfirmed = givenByMeConfirmed;
const prevAmount = contactUnconfirmed[give.agentDid] || 0; this.givenByMeUnconfirmed = givenByMeUnconfirmed;
contactUnconfirmed[give.agentDid] = prevAmount + give.amount;
} const givenToMeDescriptions = {};
if (!contactDescriptions[give.agentDid] && give.description) { const givenToMeConfirmed = {};
// Since many make the tooltip too big, we'll just use the latest. const givenToMeUnconfirmed = {};
contactDescriptions[give.agentDid] = give.description; handleResponse(
} givenToMeResp,
} givenToMeDescriptions,
} givenToMeConfirmed,
//console.log("Done retrieving receipts", contactConfirmed); givenToMeUnconfirmed,
this.givenToMeDescriptions = contactDescriptions; false,
this.givenToMeConfirmed = contactConfirmed; );
this.givenToMeUnconfirmed = contactUnconfirmed; this.givenToMeDescriptions = givenToMeDescriptions;
} else { this.givenToMeConfirmed = givenToMeConfirmed;
console.error( this.givenToMeUnconfirmed = givenToMeUnconfirmed;
"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.";
}
} catch (error) { } catch (error) {
this.alertTitle = "Error With Server"; this.$notify(
this.alertMessage = error as string; {
group: "alert",
type: "danger",
title: "Error With Server",
text: error as string,
},
-1,
);
} }
} }
@@ -403,7 +432,7 @@ export default class ContactsView extends Vue {
const allContacts = this.contacts.concat([newContact]); const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort( this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts allContacts,
); );
} }
@@ -414,7 +443,7 @@ export default class ContactsView extends Vue {
this.nameForDid(this.contacts, contact.did) + this.nameForDid(this.contacts, contact.did) +
" with DID " + " with DID " +
contact.did + contact.did +
" ?" " ?",
) )
) { ) {
await db.open(); await db.open();
@@ -428,18 +457,11 @@ export default class ContactsView extends Vue {
confirm( confirm(
"Are you sure you want to use one of your registrations for " + "Are you sure you want to use one of your registrations for " +
this.nameForDid(this.contacts, contact.did) + this.nameForDid(this.contacts, contact.did) +
"?" "?",
) )
) { ) {
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
// Make a claim
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "RegisterAction", "@type": "RegisterAction",
@@ -471,28 +493,37 @@ export default class ContactsView extends Vue {
// Make the xhr request payload // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim"; const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity); const headers = await this.getHeaders(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success?.embeddedRecordError) { if (resp.data?.success?.embeddedRecordError) {
this.alertTitle = "Registration Still Unknown";
let message = "There was some problem with the registration."; let message = "There was some problem with the registration.";
if (typeof resp.data.success.embeddedRecordError == "string") { if (typeof resp.data.success.embeddedRecordError == "string") {
message += " " + resp.data.success.embeddedRecordError; message += " " + resp.data.success.embeddedRecordError;
} }
this.alertMessage = message; this.$notify(
{
group: "alert",
type: "danger",
title: "Registration Still Unknown",
text: message,
},
-1,
);
} else if (resp.data?.success?.handleId) { } else if (resp.data?.success?.handleId) {
contact.registered = true; contact.registered = true;
db.contacts.update(contact.did, { registered: true }); db.contacts.update(contact.did, { registered: true });
this.alertTitle = "Registration Success"; this.$notify(
this.alertMessage = contact.name + " has been registered."; {
group: "alert",
type: "info",
title: "Registration Success",
text: contact.name + " has been registered.",
},
-1,
);
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error. See logs for more info.";
@@ -507,8 +538,15 @@ export default class ContactsView extends Vue {
userMessage = error as string; userMessage = error as string;
} }
// Now set that error for the user to see. // Now set that error for the user to see.
this.alertTitle = "Error With Server"; this.$notify(
this.alertMessage = userMessage; {
group: "alert",
type: "danger",
title: "Error With Server",
text: userMessage,
},
-1,
);
} }
} }
} }
@@ -519,19 +557,8 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/" + "/api/report/" +
(visibility ? "canSeeMe" : "cannotSeeMe"); (visibility ? "canSeeMe" : "cannotSeeMe");
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray(); const headers = await this.getHeaders(identity);
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const payload = JSON.stringify({ did: contact.did }); const payload = JSON.stringify({ did: contact.did });
try { try {
@@ -540,17 +567,39 @@ export default class ContactsView extends Vue {
contact.seesMe = visibility; contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility }); db.contacts.update(contact.did, { seesMe: visibility });
} else { } else {
this.alertTitle = "Error With Server";
console.error("Bad response setting visibility: ", resp.data); console.error("Bad response setting visibility: ", resp.data);
if (resp.data.error?.message) { if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message; this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: resp.data.error?.message,
},
-1,
);
} else { } else {
this.alertMessage = "Bad server response of " + resp.status; this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: "Bad server response of " + resp.status,
},
-1,
);
} }
} }
} catch (err) { } catch (err) {
this.alertTitle = "Error With Server"; this.$notify(
this.alertMessage = err as string; {
group: "alert",
type: "danger",
title: "Error With Server",
text: err as string,
},
-1,
);
} }
} }
@@ -559,19 +608,6 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" + "/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.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 || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -580,23 +616,52 @@ export default class ContactsView extends Vue {
contact.seesMe = visibility; contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility }); db.contacts.update(contact.did, { seesMe: visibility });
this.alertTitle = "Refreshed"; this.$notify(
this.alertMessage = {
this.nameForContact(contact, true) + group: "alert",
" can " + type: "toast",
(visibility ? "" : "not ") + title: "Refreshed",
"see your activity."; text:
this.nameForContact(contact, true) +
" can " +
(visibility ? "" : "not ") +
"see your activity.",
},
5000,
);
} else { } else {
this.alertTitle = "Error With Server";
if (resp.data.error?.message) { if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message; this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: resp.data.error?.message,
},
-1,
);
} else { } else {
this.alertMessage = "Bad server response of " + resp.status; this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: "Bad server response of " + resp.status,
},
-1,
);
} }
} }
} catch (err) { } catch (err) {
this.alertTitle = "Error With Server"; this.$notify(
this.alertMessage = err as string; {
group: "alert",
type: "danger",
title: "Error With Server",
text: err as string,
},
-1,
);
} }
} }
@@ -616,13 +681,7 @@ export default class ContactsView extends Vue {
} }
async onClickAddGive(fromDid: string, toDid: string): Promise<void> { async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
// if they have unconfirmed amounts, ask to confirm those first // if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) { if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
@@ -631,7 +690,7 @@ export default class ContactsView extends Vue {
"There are " + "There are " +
this.givenToMeUnconfirmed[fromDid] + this.givenToMeUnconfirmed[fromDid] +
" unconfirmed hours from them." + " 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({ this.$router.push({
@@ -641,15 +700,35 @@ export default class ContactsView extends Vue {
} }
} }
if (!this.isNumeric(this.hourInput)) { if (!this.isNumeric(this.hourInput)) {
this.alertTitle = "Input Error"; this.$notify(
this.alertMessage = {
"This is not a valid number of hours: " + this.hourInput; group: "alert",
type: "danger",
title: "Input Error",
text: "This is not a valid number of hours: " + this.hourInput,
},
-1,
);
} else if (!parseFloat(this.hourInput)) { } else if (!parseFloat(this.hourInput)) {
this.alertTitle = "Input Error"; this.$notify(
this.alertMessage = "Giving 0 hours does nothing."; {
group: "alert",
type: "danger",
title: "Input Error",
text: "Giving 0 hours does nothing.",
},
-1,
);
} else if (!identity) { } else if (!identity) {
this.alertTitle = "Status Error"; this.$notify(
this.alertMessage = "No identity is available."; {
group: "alert",
type: "danger",
title: "Status Error",
text: "No identity is available.",
},
-1,
);
} else { } else {
// ask to confirm amount // ask to confirm amount
let toFrom; let toFrom;
@@ -671,7 +750,7 @@ export default class ContactsView extends Vue {
" hours " + " hours " +
toFrom + toFrom +
description + description +
"?" "?",
) )
) { ) {
this.createAndSubmitGive( this.createAndSubmitGive(
@@ -679,7 +758,7 @@ export default class ContactsView extends Vue {
fromDid, fromDid,
toDid, toDid,
parseFloat(this.hourInput), parseFloat(this.hourInput),
this.hourDescriptionInput this.hourDescriptionInput,
); );
} }
} }
@@ -690,7 +769,7 @@ export default class ContactsView extends Vue {
fromDid: string, fromDid: string,
toDid: string, toDid: string,
amount: number, amount: number,
description: string description: string,
): Promise<void> { ): Promise<void> {
// Make a claim // Make a claim
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
@@ -728,18 +807,20 @@ export default class ContactsView extends Vue {
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim"; const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity); const headers = await this.getHeaders(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.post(url, payload, { headers }); 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?.handleId) {
this.alertTitle = "Done"; this.$notify(
this.alertMessage = "Successfully logged time to the server."; {
group: "alert",
type: "success",
title: "Done",
text: "Successfully logged time to the server.",
},
-1,
);
if (fromDid === identity.did) { if (fromDid === identity.did) {
const newList = R.clone(this.givenByMeUnconfirmed); const newList = R.clone(this.givenByMeUnconfirmed);
@@ -764,8 +845,15 @@ export default class ContactsView extends Vue {
userMessage = error as string; userMessage = error as string;
} }
// Now set that error for the user to see. // Now set that error for the user to see.
this.alertTitle = "Error With Server"; this.$notify(
this.alertMessage = userMessage; {
group: "alert",
type: "danger",
title: "Error With Server",
text: userMessage,
},
-1,
);
} }
} }
} }
@@ -783,10 +871,6 @@ export default class ContactsView extends Vue {
} }
} }
// This same popup code is in many files.
alertTitle = "";
alertMessage = "";
public showGiveAmountsClassNames() { public showGiveAmountsClassNames() {
return { return {
"bg-slate-500": this.showGiveTotals, "bg-slate-500": this.showGiveTotals,

View File

@@ -30,87 +30,80 @@
<li> <li>
<a <a
href="#" 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 Nearby
<span <span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md" class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
>20+</span >{{ localCount }}</span
> >
</a> </a>
</li> </li>
<li> <li>
<a <a
href="#" 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 Remote
<span <span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md" class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
>13</span >{{ remoteCount }}</span
> >
</a> </a>
</li> </li>
</ul> </ul>
</div> </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 --> <!-- Results List -->
<ul class=""> <InfiniteScroll @reached-bottom="loadMoreData">
<li class="border-b border-slate-300"> <ul>
<a href="project-view.html" class="block py-4 flex gap-4"> <li
<div class="w-12"> class="border-b border-slate-300"
<img v-for="project in projects"
src="https://picsum.photos/200/200?random=1" :key="project.handleId"
class="w-full rounded" >
/> <a
</div> @click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
<div class="grow"> >
<h2 class="text-base font-semibold">Canyon cleanup</h2> <div class="w-12">
<div class="text-sm"> <EntityIcon
<fa icon="user" class="fa-fw text-slate-400"></fa> Rotary :entityId="project.handleId"
:iconSize="48"
class="block border border-slate-300 rounded-md"
></EntityIcon>
</div> </div>
</div>
</a>
</li>
<li class="border-b border-slate-300"> <div class="grow">
<a href="project-view.html" class="block py-4 flex gap-4"> <h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="w-12"> <h2 class="text-base font-semibold">{{ project.name }}</h2>
<img <div class="text-sm">
src="https://picsum.photos/200/200?random=2" <fa icon="user" class="fa-fw text-slate-400"></fa>
class="w-full rounded" {{
/> didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
</div> }}
</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> </div>
</div> </a>
</a> </li>
</li> </ul>
</InfiniteScroll>
<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>
<AlertMessage <AlertMessage
:alertTitle="alertTitle" :alertTitle="alertTitle"
:alertMessage="alertMessage" :alertMessage="alertMessage"
@@ -122,74 +115,267 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as R from "ramda";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { didInfo } from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav";
import InfiniteScroll from "@/components/InfiniteScroll";
import EntityIcon from "@/components/EntityIcon";
@Component({ @Component({
components: { AlertMessage, QuickNav }, components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon },
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
searchTerms = ""; searchTerms = "";
alertMessage = "";
alertTitle = "";
projects: ProjectData[] = [];
isLocalActive = true;
isRemoteActive = false;
localCount = 0;
remoteCount = 0;
isLoading = false;
// make this function available to the Vue template
didInfo = didInfo;
async mounted() { async mounted() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
this.searchLocal();
} }
public async search() { public async buildHeaders() {
const headers = { "Content-Type": "application/json" }; const headers = { "Content-Type": "application/json" };
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, allAccounts); const account = allAccounts.find((acc) => acc.did === this.activeDid);
//console.log("about to parse from", this.activeDid, account?.identity);
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
throw new Error("No identity found."); throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
} }
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token; headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else { } else {
// it's OK without auth... we just won't get any identifiers // it's OK without auth... we just won't get any identifiers
} }
const claimContents = return headers;
"claimContents=" + encodeURIComponent(this.searchTerms);
const claimType = "claimType=PlanAction";
const queryParams = [claimContents, claimType].join("&");
return fetch(this.apiServer + "/api/v2/report/claims?" + queryParams, {
method: "GET",
headers: headers,
})
.then(async (response) => {
if (response.status !== 200) {
const details = await response.text();
throw details;
}
return response.json();
})
.then((results) => {
if (results.data) {
console.log(results.data);
} 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";
});
} }
alertMessage = ""; public async search(beforeId?: string) {
alertTitle = ""; let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
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();
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was a problem accessing the server. Please try again later. (${details})`,
},
-1,
);
throw details;
}
const results = await response.json();
const plans: ProjectData[] = results.data;
if (plans) {
for (const plan of plans) {
const { name, description, handleId, rowid, issuerDid } = plan;
this.projects.push({ name, description, handleId, rowid, issuerDid });
}
this.remoteCount = this.projects.length;
} else {
throw JSON.stringify(results);
}
} catch (e) {
console.log("Error with feed load:", e);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: e.userMessage || "There was a problem retrieving projects.",
},
-1,
);
} finally {
this.isLoading = false;
}
}
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) {
const details = await response.text();
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was a problem accessing the server. Please try again later. (${details})`,
},
-1,
);
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.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: e.userMessage || "There was a problem retrieving projects.",
},
-1,
);
} finally {
this.isLoading = false;
}
}
/**
* Data loader used by infinite scroller
* @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];
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> </script>

View File

@@ -128,6 +128,14 @@
key. key.
</p> </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"> <h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info? I know there is a record from someone, so why can't I see that info?
</h2> </h2>
@@ -146,14 +154,6 @@
page. page.
</p> </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> <h2 class="text-xl font-semibold">What is your privacy policy?</h2>
<p> <p>
See See

View File

@@ -7,22 +7,130 @@
</h1> </h1>
<div class="mb-8"> <div class="mb-8">
<h1 class="text-2xl">Quick Action</h1> <h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
<p>Choose a contact to whom to show appreciation:</p>
<!-- similar contact selection code is in multiple places --> <button
<div class="px-4"> @click="
<button this.$notify(
{
group: 'alert',
type: 'toast',
text: 'I\'m a toast. Don\'t mind me.',
},
5000,
)
"
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
>
Toast (self-dismiss)
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'info',
title: 'Information Alert',
text: 'Just wanted you to know.',
},
-1,
)
"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Info
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'success',
title: 'Success Alert',
text: 'Congratulations!',
},
-1,
)
"
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
>
Success
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'warning',
title: 'Warning Alert',
text: 'You might wanna look at this.',
},
-1,
)
"
class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
>
Warning
</button>
<button
@click="
this.$notify(
{
group: 'alert',
type: 'danger',
title: 'Danger Alert',
text: 'Something terrible has happened!',
},
-1,
)
"
class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
>
Danger
</button>
</div>
<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" v-for="contact in allContacts"
:key="contact.did" :key="contact.did"
@click="openDialog(contact)" @click="openDialog(contact)"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
> >
{{ contact.name }} <EntityIcon
</button> :entityId="contact.did"
<span v-if="allContacts.length > 0">&nbsp;or&nbsp;</span> :iconSize="64"
<button @click="openDialog()" class="text-blue-500"> class="mx-auto border border-slate-300 rounded-md mb-1"
someone not specified ></EntityIcon>
</button> <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>
</div> </div>
@@ -33,63 +141,59 @@
> >
</GiftedDialog> </GiftedDialog>
<div> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h1 class="text-2xl">Latest Activity</h1> <h2 class="text-xl font-bold mb-4">Latest Activity</h2>
<span :class="{ hidden: isHiddenSpinner }"> <div :class="{ hidden: isHiddenSpinner }">
<fa icon="spinner" class="fa-spin-pulse"></fa> <p class="text-slate-500 text-center italic mt-4 mb-4">
Loading&hellip; <fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</span> </p>
<ul> </div>
<ul class="border-t border-slate-300">
<li <li
class="border-b border-slate-300 py-2" class="border-b border-slate-300 py-2"
v-for="record in feedData" v-for="record in feedData"
:key="record.jwtId" :key="record.jwtId"
> >
<div <div
class="border-b border-dashed border-slate-400 text-orange-400 py-2 mb-2 font-bold uppercase text-sm" class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId" v-if="record.jwtId == feedLastViewedId"
> >
You've seen all claims below: You've seen all claims below:
</div> </div>
<div class="flex"> <div class="flex">
<fa <fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
icon="gift"
class="fa-fw flex-none pt-1 pr-2 text-slate-500"
></fa>
<!-- icon values: "coins" = money; "clock" = time; "gift" = others --> <!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
<span class="">{{ this.giveDescription(record) }}</span> <span class="">{{ this.giveDescription(record) }}</span>
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator";
import { Options, Vue } from "vue-class-component";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db"; import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer"; import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
@Options({ @Component({
components: { GiftedDialog, AlertMessage, QuickNav }, components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
activeDid = ""; activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
feedAllLoaded = false; feedAllLoaded = false;
feedData = []; feedData = [];
@@ -98,12 +202,44 @@ export default class HomeView extends Vue {
isHiddenSpinner = true; isHiddenSpinner = true;
alertTitle = ""; alertTitle = "";
alertMessage = ""; 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() { async created() {
try { try {
await accountsDB.open(); await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
@@ -112,21 +248,48 @@ export default class HomeView extends Vue {
this.feedLastViewedId = settings?.lastViewedClaimId; this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed(); this.updateAllFeed();
} catch (err) { } catch (err) {
this.alertTitle = "Error"; this.$notify(
this.alertMessage = {
err.userMessage || group: "alert",
"There was an error retrieving the latest sweet, sweet action."; type: "danger",
title: "Error",
text:
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.",
},
-1,
);
} }
} }
updateAllFeed = async () => { public async buildHeaders() {
this.isHiddenSpinner = false; 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) await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
.then(async (results) => { .then(async (results) => {
if (results.data.length > 0) { if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data); this.feedData = this.feedData.concat(results.data);
//console.log("Feed data:", this.feedData);
this.feedAllLoaded = results.hitLimit; this.feedAllLoaded = results.hitLimit;
this.feedPreviousOldestId = this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId; results.data[results.data.length - 1].jwtId;
@@ -134,7 +297,6 @@ export default class HomeView extends Vue {
this.feedLastViewedId == null || this.feedLastViewedId == null ||
this.feedLastViewedId < results.data[0].jwtId this.feedLastViewedId < results.data[0].jwtId
) { ) {
// save it to storage
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId, lastViewedClaimId: results.data[0].jwtId,
@@ -145,70 +307,70 @@ export default class HomeView extends Vue {
}) })
.catch((e) => { .catch((e) => {
console.log("Error with feed load:", e); console.log("Error with feed load:", e);
this.alertMessage = this.$notify(
e.userMessage || "There was an error retrieving feed data."; {
this.alertTitle = "Error"; group: "alert",
type: "danger",
title: "Export Error",
text: e.userMessage || "There was an error retrieving feed data.",
},
-1,
);
}); });
this.isHiddenSpinner = true; this.isHiddenSpinner = true;
}; }
retrieveClaims = async (endorserApiServer, identifier, beforeId) => { public async retrieveClaims(endorserApiServer, identifier, beforeId) {
//const afterQuery = afterId == null ? "" : "&afterId=" + afterId;
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const headers = { "Content-Type": "application/json" }; const response = await fetch(
if (this.activeDid) { endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
const account = R.find( {
(acc) => acc.did === this.activeDid, method: "GET",
this.allAccounts headers: await this.buildHeaders(),
); },
const identity = JSON.parse(account?.identity || "null"); );
if (!identity) {
throw new Error("No identity found."); if (response.status !== 200) {
} throw await response.text();
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
} else {
// it's OK without auth... we just won't get any identifiers
} }
return fetch(this.apiServer + "/api/v2/report/gives?" + beforeQuery, {
method: "GET", const results = await response.json();
headers: headers,
}) if (results.data) {
.then(async (response) => { return results;
if (response.status !== 200) { } else {
const details = await response.text(); throw JSON.stringify(results);
throw details; }
} }
return response.json();
})
.then((results) => {
if (results.data) {
return results;
} else {
throw JSON.stringify(results);
}
});
};
giveDescription(giveRecord) { giveDescription(giveRecord) {
let claim = giveRecord.fullClaim; let claim = giveRecord.fullClaim;
if (claim.claim) { if (claim.claim) {
// it's probably a Verified Credential
claim = claim.claim; claim = claim.claim;
} }
// agent.did is for legacy data, before March 2023 // agent.did is for legacy data, before March 2023
const giver = const giverDid =
claim.agent?.identifier || claim.agent?.did || giveRecord.issuer; claim.agent?.identifier || claim.agent?.did;
const giverInfo = didInfo(giver, this.allAccounts, this.allContacts); const giverInfo = didInfo(
giverDid,
this.activeDid,
this.allMyDids,
this.allContacts,
);
const gaveAmount = claim.object?.amountOfThisGood const gaveAmount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.description || "something unknown"; : claim.description || "something unknown";
// recipient.did is for legacy data, before March 2023 // recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did; const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientInfo = gaveRecipientId const gaveRecipientInfo = gaveRecipientId
? " to " + didInfo(gaveRecipientId, this.allAccounts, this.allContacts) ? " to " +
didInfo(
gaveRecipientId,
this.activeDid,
this.allMyDids,
this.allContacts,
)
: ""; : "";
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo; return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
} }
@@ -224,6 +386,7 @@ export default class HomeView extends Vue {
openDialog(giver) { openDialog(giver) {
this.$refs.customDialog.open(giver); this.$refs.customDialog.open(giver);
} }
handleDialogResult(result) { handleDialogResult(result) {
if (result.action === "confirm") { if (result.action === "confirm") {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -241,59 +404,101 @@ export default class HomeView extends Vue {
* @param description may be an empty string * @param description may be an empty string
* @param hours may be 0 * @param hours may be 0
*/ */
recordGive(giverDid, description, hours) { public async recordGive(giverDid, description, hours) {
if (this.activeDid == null) { if (!this.activeDid) {
this.alertTitle = "Error"; this.$notify(
this.alertMessage = {
"You must select an identity before you can record a give."; group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record a give.",
},
-1,
);
return; return;
} }
if (!description && !hours) { if (!description && !hours) {
this.alertTitle = "Error"; this.$notify(
this.alertMessage = {
"You must enter a description or some number of hours."; group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
},
-1,
);
return; return;
} }
const account = R.find(
(acc) => acc.did === this.activeDid, try {
this.allAccounts const identity = await this.getIdentity(this.activeDid);
); const result = await createAndSubmitGive(
//console.log("about to parse from", this.activeDid, account?.identity); this.axios,
const identity = JSON.parse(account?.identity || "null"); this.apiServer,
if (!identity) { identity,
throw new Error("No identity found."); giverDid,
this.activeDid,
description,
hours,
);
if (this.isGiveCreationError(result)) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error recording the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That gift was recorded.",
},
-1,
);
}
} catch (error) {
console.log("Error with give caught:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
this.getGiveErrorMessage(error) ||
"There was an error recording the give.",
},
-1,
);
} }
createAndSubmitGive( }
this.axios,
this.apiServer, private setAlert(title, message) {
identity, this.alertTitle = title;
giverDid, this.alertMessage = message;
this.activeDid, }
description,
hours // Helper functions for readability
)
.then((result) => { isGiveCreationError(result) {
if (result.status != 201 || result.data?.error) { return result.status !== 201 || result.data?.error;
console.log("Error with give result:", result); }
this.alertTitle = "Error";
this.alertMessage = getGiveCreationErrorMessage(result) {
result.data?.error?.message || return result.data?.error?.message;
"There was an error recording the give."; }
} else {
this.alertTitle = "Success"; getGiveErrorMessage(error) {
this.alertMessage = "That gift was recorded."; return error.userMessage || error.response?.data?.error?.message;
//this.updateAllFeed(); // full update is overkill but we should show something
}
})
.catch((e) => {
// axios throws errors on 400 responses
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.";
});
} }
} }
</script> </script>

View File

@@ -0,0 +1,175 @@
<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.$notify(
{
group: "alert",
type: "danger",
title: "Error Creating Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error(
"Telling user to clear cache at page create because:",
err,
);
}
}
}
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> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Options({ @Component({
components: {}, components: {},
}) })
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {
@@ -72,7 +72,7 @@ export default class ImportAccountView extends Vue {
this.address, this.address,
this.publicHex, this.publicHex,
this.privateHex, this.privateHex,
this.derivationPath this.derivationPath,
); );
try { try {

View File

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

View File

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

View File

@@ -73,7 +73,6 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
@@ -83,14 +82,6 @@ import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
interface VerifiableCredential {
"@context": string;
"@type": string;
name: string;
description: string;
identifier?: string;
}
@Component({ @Component({
components: { AlertMessage }, components: { AlertMessage },
}) })
@@ -100,12 +91,44 @@ export default class NewEditProjectView extends Vue {
projectName = ""; projectName = "";
description = ""; description = "";
errorMessage = ""; 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") || ""; projectId = localStorage.getItem("projectId") || "";
isHiddenSave = false; isHiddenSave = false;
isHiddenSpinner = true; isHiddenSpinner = true;
// 'created' hook runs when the Vue instance is first created
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -113,16 +136,14 @@ export default class NewEditProjectView extends Vue {
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
if (this.projectId) { if (this.projectId) {
await accountsDB.open(); if (this.numAccounts === 0) {
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.error("Error: no account was found."); console.error("Error: no account was found.");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const identity = await this.getIdentity(this.activeDid);
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
throw new Error("No identity found."); 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); this.LoadProject(identity);
} }
@@ -197,15 +218,15 @@ export default class NewEditProjectView extends Vue {
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
// handleId is new in server v release-1.6.0; remove fullIri when that // handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/ // version shows up here: https://api.endorser.ch/api-docs/
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) { if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
this.errorMessage = ""; this.errorMessage = "";
this.alertTitle = ""; this.alertTitle = "";
this.alertMessage = ""; this.alertMessage = "";
// handleId is new in server v release-1.6.0; remove fullIri when that // handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/ // version shows up here: https://api.endorser.ch/api-docs/
useAppStore().setProjectId( useAppStore().setProjectId(
resp.data.success.handleId || resp.data.success.fullIri resp.data.success.handleId || resp.data.success.fullIri,
); );
setTimeout( setTimeout(
function (that: Vue) { function (that: Vue) {
@@ -215,7 +236,7 @@ export default class NewEditProjectView extends Vue {
that.$router.push(route); that.$router.push(route);
}, },
2000, 2000,
this this,
); );
} }
} catch (error) { } catch (error) {
@@ -224,20 +245,41 @@ export default class NewEditProjectView extends Vue {
if (serverError) { if (serverError) {
if (Object.prototype.hasOwnProperty.call(serverError, "message")) { if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError); console.log(serverError);
this.alertTitle = "User Message";
userMessage = serverError.response.data.error.message; // This is info for the user. userMessage = serverError.response.data.error.message; // This is info for the user.
this.alertMessage = userMessage; this.$notify(
{
group: "alert",
type: "danger",
title: "User Message",
text: userMessage,
},
-1,
);
} else { } else {
this.alertTitle = "Server Message"; this.$notify(
this.alertMessage = JSON.stringify(serverError.toJSON()); {
group: "alert",
type: "danger",
title: "Server Message",
text: JSON.stringify(serverError.toJSON()),
},
-1,
);
} }
} else { } else {
console.error( console.error(
"Here's the full error trying to save the claim:", "Here's the full error trying to save the claim:",
error error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Claim Error",
text: error as string,
},
-1,
); );
this.alertTitle = "Claim Error";
this.alertMessage = error as string;
} }
// Now set that error for the user to see. // Now set that error for the user to see.
this.errorMessage = userMessage; this.errorMessage = userMessage;
@@ -248,17 +290,11 @@ export default class NewEditProjectView extends Vue {
public async onSaveProjectClick() { public async onSaveProjectClick() {
this.isHiddenSave = true; this.isHiddenSave = true;
this.isHiddenSpinner = false; this.isHiddenSpinner = false;
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count(); if (this.numAccounts === 0) {
if (num_accounts === 0) {
console.error("Error: there is no account."); console.error("Error: there is no account.");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const identity = await this.getIdentity(this.activeDid);
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.SaveProject(identity); this.SaveProject(identity);
} }
} }
@@ -266,9 +302,5 @@ export default class NewEditProjectView extends Vue {
public onCancelClick() { public onCancelClick() {
this.$router.back(); this.$router.back();
} }
// This same popup code is in many files.
alertTitle = "";
alertMessage = "";
} }
</script> </script>

View File

@@ -50,7 +50,6 @@ export default class AccountViewView extends Vue {
loading = true; loading = true;
async mounted() { async mounted() {
await accountsDB.open();
const mnemonic = generateSeed(); const mnemonic = generateSeed();
// address is 0x... ETH address, without "did:eth:" // address is 0x... ETH address, without "did:eth:"
const [address, privateHex, publicHex, derivationPath] = const [address, privateHex, publicHex, derivationPath] =
@@ -58,6 +57,8 @@ export default class AccountViewView extends Vue {
const newId = newIdentifier(address, publicHex, privateHex, derivationPath); const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
const identity = JSON.stringify(newId); const identity = JSON.stringify(newId);
await accountsDB.open();
await accountsDB.accounts.add({ await accountsDB.accounts.add({
dateCreated: new Date().toISOString(), dateCreated: new Date().toISOString(),
derivationPath: derivationPath, derivationPath: derivationPath,

View File

@@ -23,20 +23,31 @@
</h1> </h1>
</div> </div>
<div>
{{ errorMessage }}
</div>
<!-- Project Details --> <!-- Project Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<div> <div>
<h2 class="text-xl font-semibold">{{ name }}</h2> <div class="block pb-4 flex gap-4">
<div class="flex justify-between gap-4 text-sm mb-3"> <div class="flex-none w-16 pt-1">
<span><fa icon="user" class="fa-fw text-slate-400"></fa> Rotary</span> <EntityIcon
<span :entityId="projectId"
><fa icon="calendar" class="fa-fw text-slate-400"></fa :iconSize="64"
>{{ timeSince }} class="block border border-slate-300 rounded-md"
</span> ></EntityIcon>
</div>
<div class="overflow-hidden">
<h2 class="text-xl font-semibold">{{ name }}</h2>
<div class="text-sm mb-3">
<div class="truncate"
><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ issuer }}</div
>
<div
><fa icon="calendar" class="fa-fw text-slate-400"></fa
>{{ timeSince }}
</div>
</div>
</div>
</div> </div>
<div class="text-sm text-slate-500"> <div class="text-sm text-slate-500">
@@ -65,29 +76,113 @@
</button> </button>
</div> </div>
<button
@click="openDialog({ name: 'you', did: activeDid })"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
>
I gave...
</button>
<div> <div>
<p>... or choose a contact who gave:</p> <div v-if="activeDid" class="text-center">
<!-- similar contact selection code is in multiple places -->
<div class="px-4">
<button <button
@click="openDialog({ name: 'you', did: activeDid })"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
>
I gave&hellip;
</button>
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
</div>
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()">
<div class="mb-1">
<fa icon="question-circle" class="fa-fw fa-xl text-slate-400"></fa>
</div>
<h3
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
Anonymous
</h3>
</li>
<li
v-for="contact in allContacts" v-for="contact in allContacts"
:key="contact.did" :key="contact.did"
@click="openDialog(contact)" @click="openDialog(contact)"
class="text-blue-500"
> >
&nbsp;{{ contact.name }}, <div class="mb-1">
</button> <fa icon="user" class="fa-fw fa-xl text-slate-400"></fa>
<span v-if="allContacts.length > 0">&nbsp;or&nbsp;</span> </div>
<button @click="openDialog()" class="text-blue-500"> <h3
someone not specified class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
</button> >
{{ 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>
</div>
<!-- Gifts to & from this -->
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">
Given to this Project
</h3>
<ul class="text-sm border-t border-slate-300">
<li
v-for="give in givesToThis"
:key="give.id"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
<span
><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
</span>
<span v-if="give.amount"
><fa icon="coins" class="fa-fw text-slate-400"></fa>
{{ give.amount }}
</span>
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400"></fa>
{{ give.description }}
</div>
</li>
</ul>
</div>
<div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">
&hellip;and from this Project
</h3>
<ul class="text-sm border-t border-slate-300">
<li
v-for="give in givesByThis"
:key="give.id"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
<span
><fa icon="user" class="fa-fw text-slate-400"></fa>
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
</span>
<span v-if="give.amount"
><fa icon="coins" class="fa-fw text-slate-400"></fa>
{{ give.amount }}
</span>
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400"></fa>
{{ give.description }}
</div>
</li>
</ul>
</div> </div>
</div> </div>
@@ -97,45 +192,6 @@
message="Received from" message="Received from"
> >
</GiftedDialog> </GiftedDialog>
<!-- 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>
-->
<AlertMessage <AlertMessage
:alertTitle="alertTitle" :alertTitle="alertTitle"
:alertMessage="alertMessage" :alertMessage="alertMessage"
@@ -146,34 +202,84 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as moment from "moment"; import * as moment from "moment";
import * as R from "ramda"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { createAndSubmitGive } from "@/libs/endorserServer";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import {
createAndSubmitGive,
didInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
@Component({ @Component({
components: { GiftedDialog, AlertMessage, QuickNav }, components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
}) })
export default class ProjectViewView extends Vue { export default class ProjectViewView extends Vue {
activeDid = ""; activeDid = "";
alertMessage = "";
alertTitle = "";
allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
apiServer = ""; apiServer = "";
expanded = false;
name = "";
description = ""; description = "";
expanded = false;
givesToThis: Array<GiveServerRecord> = [];
givesByThis: Array<GiveServerRecord> = [];
name = "";
issuer = "";
projectId = localStorage.getItem("projectId") || ""; // handle ID
timeSince = "";
truncatedDesc = ""; truncatedDesc = "";
truncateLength = 40; truncateLength = 40;
timeSince = "";
projectId = localStorage.getItem("projectId") || ""; // handle ID async created() {
errorMessage = ""; 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() { onEditClick() {
localStorage.setItem("projectId", this.projectId as string); localStorage.setItem("projectId", this.projectId as string);
@@ -183,6 +289,11 @@ export default class ProjectViewView extends Vue {
this.$router.push(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() { expandText() {
this.expanded = true; this.expanded = true;
} }
@@ -196,11 +307,13 @@ export default class ProjectViewView extends Vue {
this.apiServer + this.apiServer +
"/api/claim/byHandle/" + "/api/claim/byHandle/" +
encodeURIComponent(this.projectId); encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token,
}; };
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -211,52 +324,125 @@ export default class ProjectViewView extends Vue {
const now = moment.now(); const now = moment.now();
this.timeSince = moment.utc(now).to(eventDate); this.timeSince = moment.utc(now).to(eventDate);
} }
this.issuer = resp.data.issuer;
this.name = resp.data.claim?.name || "(no name)"; this.name = resp.data.claim?.name || "(no name)";
this.description = resp.data.claim?.description || "(no description)"; this.description = resp.data.claim?.description || "(no description)";
this.truncatedDesc = this.description.slice(0, this.truncateLength); this.truncatedDesc = this.description.slice(0, this.truncateLength);
} else if (resp.status === 404) { } else if (resp.status === 404) {
// actually, axios throws an error so we never get here // actually, axios throws an error so we never get here
this.errorMessage = "That project does not exist."; this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "That project does not exist.",
},
-1,
);
} }
} catch (error: unknown) { } catch (error: unknown) {
const serverError = error as AxiosError; const serverError = error as AxiosError;
if (serverError.response?.status === 404) { if (serverError.response?.status === 404) {
this.errorMessage = "That project does not exist."; this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "That project does not exist.",
},
-1,
);
} else { } else {
this.errorMessage = this.$notify(
"Something went wrong retrieving that project." + {
" See logs for more info."; group: "alert",
console.error("Error retrieving project:", error); type: "danger",
title: "Error",
text: "Something went wrong retrieving that project. See logs for more info.",
},
-1,
);
console.error("Error retrieving project:", serverError.message);
} }
} }
}
// 'created' hook runs when the Vue instance is first created const givesInUrl =
async created() { this.apiServer +
await db.open(); "/api/v2/report/givesForPlans?planIds=" +
const settings = await db.settings.get(MASTER_SETTINGS_KEY); encodeURIComponent(JSON.stringify([this.projectId]));
this.activeDid = settings?.activeDid || ""; try {
this.apiServer = settings?.apiServer || ""; const resp = await this.axios.get(givesInUrl, { headers });
this.allContacts = await db.contacts.toArray(); if (resp.status === 200 && resp.data.data) {
this.givesToThis = resp.data.data;
await accountsDB.open(); } else {
const num_accounts = await accountsDB.accounts.count(); this.$notify(
if (num_accounts === 0) { {
console.error("Problem! Should have a profile!"); group: "alert",
} else { type: "danger",
const accounts = await accountsDB.accounts.toArray(); title: "Error",
const account = R.find((acc) => acc.did === this.activeDid, accounts); text: "Failed to retrieve gives to this project.",
const identity = JSON.parse(account?.identity || "null"); },
if (!identity) { -1,
throw new Error("No identity found."); );
} }
this.LoadProject(identity); } catch (error: unknown) {
const serverError = error as AxiosError;
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving gives to this project.",
},
-1,
);
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.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to retrieve gives by this project.",
},
-1,
);
}
} catch (error: unknown) {
const serverError = error as AxiosError;
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving gives by project.",
},
-1,
);
console.error(
"Error retrieving gives by this project:",
serverError.message,
);
} }
} }
openDialog(contact) { openDialog(contact) {
this.$refs.customDialog.open(contact); this.$refs.customDialog.open(contact);
} }
handleDialogResult(result) { handleDialogResult(result) {
if (result.action === "confirm") { if (result.action === "confirm") {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -275,60 +461,84 @@ export default class ProjectViewView extends Vue {
* @param hours may be 0 * @param hours may be 0
*/ */
async recordGive(giverDid, description, hours) { async recordGive(giverDid, description, hours) {
if (this.activeDid == null) { if (!this.activeDid) {
this.alertTitle = "Error"; this.$notify(
this.alertMessage = {
"You must select an identity before you can record a give."; group: "alert",
type: "danger",
title: "Error",
text: "You must select an identity before you can record a give.",
},
-1,
);
return; return;
} }
if (!description && !hours) {
this.alertTitle = "Error";
this.alertMessage =
"You must enter a description or some number of hours.";
return;
}
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
this.projectId
)
.then((result) => {
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.";
//this.updateAllFeed(); // full update is overkill but we should show something
}
})
.catch((e) => {
// axios throws errors on 400 responses
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.";
});
}
// This same popup code is in many files. if (!description && !hours) {
alertMessage = ""; this.$notify(
alertTitle = ""; {
group: "alert",
type: "danger",
title: "Error",
text: "You must enter a description or some number of hours.",
},
-1,
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await 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.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
result.data?.error?.message ||
"There was an error recording the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "That gift was recorded.",
},
-1,
);
}
} catch (e) {
console.log("Error with give caught:", e);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
e.userMessage ||
e.response?.data?.error?.message ||
"There was an error recording the give.",
},
-1,
);
}
}
} }
</script> </script>

View File

@@ -7,7 +7,8 @@
</h1> </h1>
<!-- Quick Search --> <!-- Quick Search -->
<form id="QuickSearch" class="mb-4 flex">
<div id="QuickSearch" class="mb-4 flex">
<input <input
type="text" type="text"
placeholder="Search…" placeholder="Search…"
@@ -18,7 +19,7 @@
> >
<fa icon="magnifying-glass" class="fa-fw"></fa> <fa icon="magnifying-glass" class="fa-fw"></fa>
</button> </button>
</form> </div>
<!-- New Project --> <!-- New Project -->
<button <button
@@ -38,7 +39,7 @@
<!-- Results List --> <!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData"> <InfiniteScroll @reached-bottom="loadMoreData">
<ul> <ul class="border-t border-slate-300">
<li <li
class="border-b border-slate-300" class="border-b border-slate-300"
v-for="project in projects" v-for="project in projects"
@@ -49,10 +50,11 @@
class="block py-4 flex gap-4" class="block py-4 flex gap-4"
> >
<div class="flex-none w-12"> <div class="flex-none w-12">
<img <EntityIcon
src="https://picsum.photos/200/200?random=1" :entityId="project.handleId"
class="w-full rounded" :iconSize="48"
/> class="inline-block align-middle border border-slate-300 rounded-md"
></EntityIcon>
</div> </div>
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
@@ -73,7 +75,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@@ -82,31 +83,10 @@ import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll"; import InfiniteScroll from "@/components/InfiniteScroll";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
/**
* Represents data about a project
**/
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;
}
@Component({ @Component({
components: { InfiniteScroll, AlertMessage, QuickNav }, components: { InfiniteScroll, AlertMessage, QuickNav, EntityIcon },
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
apiServer = ""; apiServer = "";
@@ -115,6 +95,12 @@ export default class ProjectsView extends Vue {
isLoading = false; isLoading = false;
alertTitle = ""; alertTitle = "";
alertMessage = ""; alertMessage = "";
numAccounts = 0;
async beforeCreate() {
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
}
/** /**
* Core project data loader * Core project data loader
@@ -130,19 +116,27 @@ export default class ProjectsView extends Vue {
try { try {
this.isLoading = true; this.isLoading = true;
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200) { if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data; const plans: ProjectData[] = resp.data.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId = plan.fullIri, rowid } = plan; const { name, description, handleId = plan.fullIri, rowid } = plan;
this.projects.push({ name, description, handleId, rowid }); this.projects.push({ name, description, handleId, rowid });
} }
} else { } else {
console.log(resp.status); console.log("Bad server response & data:", resp.status, resp.data);
throw Error("Failed to get projects from the server.");
} }
} catch (error) { } catch (error) {
console.error("Got error loading projects:", error.message); console.error("Got error loading projects:", error.message);
this.alertTitle = "Error"; this.$notify(
this.alertMessage = "Got an error loading projects:" + error.message; {
group: "alert",
type: "danger",
title: "Error",
text: "Got an error loading projects: " + error.message,
},
-1,
);
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
@@ -183,6 +177,22 @@ export default class ProjectsView extends Vue {
await this.dataLoader(url, token); await this.dataLoader(url, token);
} }
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;
}
/** /**
* 'created' hook runs when the Vue instance is first created * 'created' hook runs when the Vue instance is first created
**/ **/
@@ -193,26 +203,33 @@ export default class ProjectsView extends Vue {
const activeDid = settings?.activeDid || ""; const activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
await accountsDB.open(); if (this.numAccounts === 0) {
const num_accounts = await accountsDB.accounts.count(); console.error("No accounts found.");
if (num_accounts === 0) { this.$notify(
console.error("Problem! You need a profile!"); {
this.alertTitle = "Error!"; group: "alert",
this.alertMessage = "Problem! You need a profile!"; type: "danger",
title: "Error",
text: "You need an identity to load your projects.",
},
-1,
);
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const identity = await this.getIdentity(activeDid);
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.current = identity; this.current = identity;
this.LoadProjects(identity); this.LoadProjects(identity);
} }
} catch (err) { } catch (err) {
console.log(err); console.log("Error initializing:", err);
this.alertTitle = "Error!"; this.$notify(
this.alertMessage = "Problem! You need a profile!"; {
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong loading your projects.",
},
-1,
);
} }
} }

View File

@@ -20,22 +20,26 @@
</div> </div>
<div v-if="activeAccount"> <div v-if="activeAccount">
<p> <p class="text-center mb-4">
BEWARE: Anyone who gets hold of this mnemonic seed phrase will be able <b class="text-red-600">BEWARE!</b> Anyone who has this seed phrase will
impersonate you and take over any digital holdings based on it. So only be able impersonate you and take over any digital holdings based on it.
reveal it when you are in a private place out of sight of other eyes, Reveal it when you are somewhere only you can see your screen, and
and only record it in something private -- don't take a screenshot or record it somewhere only you have access.
send it to any online service. <i>Don't take a screenshot or send it to any online service.</i>
</p> </p>
<button <div class="bg-slate-100 rounded-md overflow-hidden p-4 mb-4">
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" <button
@click="showSeedPhrase" 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> Reveal my Seed Phrase
</button>
<p v-if="showSeed">{{ activeAccount.mnemonic }}</p> <p v-if="showSeed" class="text-center text-slate-700 mt-2">
{{ activeAccount.mnemonic }}
</p>
</div>
</div> </div>
<div v-else>You do not have an active identity.</div> <div v-else>You do not have an active identity.</div>
<AlertMessage <AlertMessage
@@ -72,8 +76,15 @@ export default class SeedBackupView extends Vue {
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts); this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
} catch (err) { } catch (err) {
console.error("Got an error loading an identity:", err); console.error("Got an error loading an identity:", err);
this.alertTitle = "Error Loading Account"; this.$notify(
this.alertMessage = "Got an error loading your seed data."; {
group: "alert",
type: "danger",
title: "Error Loading Account",
text: "Got an error loading your seed data.",
},
-1,
);
} }
} }

View File

@@ -28,9 +28,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
@Options({ @Component({
components: {}, components: {},
}) })
export default class StartView extends Vue { export default class StartView extends Vue {

View File

@@ -39,15 +39,7 @@
:alertMessage="alertMessage" :alertMessage="alertMessage"
></AlertMessage> ></AlertMessage>
</section> </section>
</template> /** </template>
// from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#examples
// Adds a blank image const dataBlob = document
.querySelector("#scene-container") .firstChild.toBlob((blob) => { const newImg =
document.createElement("img"); const url = URL.createObjectURL(blob);
newImg.onload = () => { // no longer need to read the blob so it's revoked
URL.revokeObjectURL(url); }; newImg.src = url;
document.body.appendChild(newImg); }); **/
<script lang="ts"> <script lang="ts">
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js"; import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
@@ -55,11 +47,6 @@ import { World } from "@/components/World/World.js";
import AlertMessage from "@/components/AlertMessage"; import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav";
interface WorldProperties {
startTime?: string;
endTime?: string;
}
@Component({ components: { AlertMessage, World, QuickNav } }) @Component({ components: { AlertMessage, World, QuickNav } })
export default class StatisticsView extends Vue { export default class StatisticsView extends Vue {
world: World; world: World;
@@ -70,14 +57,20 @@ export default class StatisticsView extends Vue {
mounted() { mounted() {
try { try {
const container = document.querySelector("#scene-container"); const container = document.querySelector("#scene-container");
console.log(container);
const newWorld = new World(container, this); const newWorld = new World(container, this);
newWorld.start(); newWorld.start();
this.world = newWorld; this.world = newWorld;
} catch (err) { } catch (err) {
console.log(err); console.log(err);
this.alertTitle = "Mounting error"; this.$notify(
this.alertMessage = err.message; {
group: "alert",
type: "danger",
title: "Mounting Error",
text: err.message,
},
-1,
);
} }
} }