forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
73 Commits
remove-old
...
more-small
| Author | SHA1 | Date | |
|---|---|---|---|
| 85bd807bcc | |||
| eeece8a1b4 | |||
| bbfc1e1007 | |||
| 433d0c023e | |||
| ac6376243b | |||
| a12f033b72 | |||
| 42cd7d00de | |||
| c388cc8cfe | |||
| 6d4d4e40c3 | |||
| 3b39faf173 | |||
| f43ecc98aa | |||
| 5b7ccf9ef0 | |||
| 9bacd4da87 | |||
| 7450d8d1c3 | |||
| 7490cfc557 | |||
| 95287e4dd0 | |||
| 679d1a70e8 | |||
| 047fb263dd | |||
| b76cf28bc2 | |||
| 58c091cdaa | |||
| 0df5a975f3 | |||
| 94051e6ba9 | |||
| 8e60f53f0b | |||
| afc48a5434 | |||
| 6eb3381a98 | |||
| 2bec218cc5 | |||
| 327c655fb3 | |||
| 866aad069f | |||
| 7f6c938029 | |||
| 6d2df4a50c | |||
| 7305606546 | |||
| 2a9ff8aa77 | |||
| 829994491c | |||
| ce06e8f0fa | |||
| 1ee751eea8 | |||
| 116b239616 | |||
| f47346cc35 | |||
| 0e02268950 | |||
| 94d9c425ad | |||
| 2db662c125 | |||
| b7892f4dfa | |||
|
|
3bbb138299 | ||
|
|
5b5c631001 | ||
|
|
e60b56a0b0 | ||
|
|
d3e025c293 | ||
|
|
6f4027f614 | ||
| 249811efe3 | |||
| bd2455458f | |||
| a053c48819 | |||
| 9486142b2a | |||
|
|
2fba7f2a55 | ||
|
|
31d13b9143 | ||
|
|
852bd93f3f | ||
| b707bfce40 | |||
| bdb8e2e32a | |||
| 06b173e861 | |||
| 6a8b9d36a7 | |||
| 52a6451a2d | |||
| 4b9cbd0e9f | |||
| a5e0c847b1 | |||
|
|
fd43da93a5 | ||
| b59bcf249a | |||
| b05b602acd | |||
| b8aaffbf8d | |||
|
|
5501ac1a2f | ||
|
|
b514d64068 | ||
|
|
c4537420b4 | ||
|
|
5f50338dd0 | ||
| 308386d829 | |||
| 999d7abc04 | |||
| f7f947bfdd | |||
| 26d9b134c7 | |||
| 8ee610c1bc |
14
CHANGELOG.md
Normal file
14
CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [0.1.2] - 2023.11.01
|
||||
### Added
|
||||
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
||||
21
README.md
21
README.md
@@ -1,6 +1,9 @@
|
||||
# kickstart-for-time-pwa
|
||||
|
||||
## Project setup
|
||||
|
||||
We have pkgx.dev set up in package.json, so you can use `dev` to set up the dev environment.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
@@ -23,6 +26,12 @@ npm run build
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
###
|
||||
|
||||
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
|
||||
|
||||
### Test key contents
|
||||
|
||||
See [this page](openssl_signing_console.rst)
|
||||
@@ -86,19 +95,13 @@ Clear cache for localhost, then go to http://localhost:8080/start
|
||||
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
See https://tea.xyz
|
||||
|
||||
| Project | Version |
|
||||
| ---------- | --------- |
|
||||
| nodejs.org | ^16.0.0 |
|
||||
| npmjs.com | ^8.0.0 |
|
||||
|
||||
## Other
|
||||
|
||||
### Reference Material
|
||||
|
||||
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
|
||||
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
|
||||
|
||||
```
|
||||
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
|
||||
|
||||
|
||||
32384
package-lock.json
generated
32384
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kickstart-for-time-pwa",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
@@ -52,11 +52,12 @@
|
||||
"vue": "^3.3.4",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-facing-decorator": "^2.1.20",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-qrcode-reader": "^5.3.4",
|
||||
"vue-router": "^4.2.3",
|
||||
"web-did-resolver": "^2.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/leaflet": "^1.9.4",
|
||||
"@types/ramda": "^0.29.3",
|
||||
"@types/three": "^0.152.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||
@@ -80,5 +81,6 @@
|
||||
"prettier": "^3.0.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "~5.1.6"
|
||||
}
|
||||
},
|
||||
"pkgx": "node^18 npm^10"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
|
||||
tasks:
|
||||
|
||||
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
|
||||
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
|
||||
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
|
||||
- 40 notifications :
|
||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||
|
||||
- 01 add my bounding box(es) of interest for searches on Nearby part of Discovery page
|
||||
- .5 search by a bounding box(s) of interest for local projects (see API by clicking on "Nearby")
|
||||
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
||||
|
||||
- 08 Scan QR code to import into contacts assignee:matthew
|
||||
- SEE: https://github.com/gruhn/vue-qrcode-reader
|
||||
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
||||
- .1 add instructions for map location selection
|
||||
|
||||
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
|
||||
|
||||
@@ -24,16 +23,23 @@ tasks:
|
||||
|
||||
- 24 Move to Vite assignee:matthew
|
||||
|
||||
- .5 include the hash of the latest commit, and maybe a version
|
||||
- .2 fit as many icons as possible on home & project view screens but only going halfway down the page
|
||||
- .1 Remove notification alert visuals on home page
|
||||
- .5 Add infinite scroll to gifts on the home page
|
||||
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
||||
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
|
||||
- .1 when creating a plan, select location and then make sure you can deselect on Android
|
||||
- .5 include a version, maybe the hash of the latest commit -- figuring out how it works on prod now
|
||||
- .5 add link to further project / people when a project pays ahead
|
||||
- .5 add project ID to the URL, to make a project publicly-accessible
|
||||
- .5 remove edit from project page for projects owned by others
|
||||
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
|
||||
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist assignee-group:ui
|
||||
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui
|
||||
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
||||
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
||||
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||
- .1 Make give description text box into something that expands as they type
|
||||
- .1 Make contact info specific to Time Safari - rather pointing at CommunityCred.org
|
||||
|
||||
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
||||
|
||||
@@ -46,6 +52,9 @@ tasks:
|
||||
- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui
|
||||
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
||||
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
|
||||
- .5 make a VC details page
|
||||
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
||||
- .1 remove firstName (& lastName) from localStorage
|
||||
|
||||
- contacts v+ :
|
||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||
@@ -69,6 +78,11 @@ tasks:
|
||||
- Test PWA features on Android and iOS.
|
||||
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
||||
|
||||
- .5 show seed phrase in a QR code for transfer to another device
|
||||
|
||||
- 32 accept images for projects
|
||||
- 32 accept images for contacts
|
||||
|
||||
- linking between projects or plans :
|
||||
- show total time given to & from a project
|
||||
- terminology:
|
||||
@@ -102,6 +116,8 @@ tasks:
|
||||
- 40 notifications v+ :
|
||||
- pull, w/ scheduled runs
|
||||
|
||||
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
||||
|
||||
log:
|
||||
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
||||
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27
|
||||
|
||||
177
sample.txt
Normal file
177
sample.txt
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
> kickstart-for-time-pwa@0.1.0 build
|
||||
> vue-cli-service build
|
||||
|
||||
All browser targets in the browserslist configuration have supported ES module.
|
||||
Therefore we don't build two separate bundles for differential loading.
|
||||
|
||||
|
||||
WARNING Compiled with 5 warnings6:06:43 PM
|
||||
|
||||
[eslint]
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/components/World/components/objects/landmarks.js
|
||||
98:11 warning Unexpected console statement no-console
|
||||
133:7 warning Unexpected console statement no-console
|
||||
144:5 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/router/index.ts
|
||||
210:3 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/AccountViewView.vue
|
||||
362:7 warning Unexpected console statement no-console
|
||||
375:7 warning Unexpected console statement no-console
|
||||
404:7 warning Unexpected console statement no-console
|
||||
516:7 warning Unexpected console statement no-console
|
||||
536:7 warning Unexpected console statement no-console
|
||||
630:5 warning Unexpected console statement no-console
|
||||
682:7 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactAmountsView.vue
|
||||
206:9 warning Unexpected console statement no-console
|
||||
233:9 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactGiftingView.vue
|
||||
244:9 warning Unexpected console statement no-console
|
||||
267:7 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactsView.vue
|
||||
340:9 warning Unexpected console statement no-console
|
||||
577:9 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/DiscoverView.vue
|
||||
315:9 warning Unexpected console statement no-console
|
||||
343:7 warning Unexpected console statement no-console
|
||||
390:9 warning Unexpected console statement no-console
|
||||
423:7 warning Unexpected console statement no-console
|
||||
532:9 warning Unexpected console statement no-console
|
||||
575:7 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/HomeView.vue
|
||||
349:9 warning Unexpected console statement no-console
|
||||
498:9 warning Unexpected console statement no-console
|
||||
521:7 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/IdentitySwitcherView.vue
|
||||
142:7 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportAccountView.vue
|
||||
123:9 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportDerivedAccountView.vue
|
||||
159:7 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/NewEditProjectView.vue
|
||||
183:9 warning Unexpected console statement no-console
|
||||
215:7 warning Unexpected console statement no-console
|
||||
297:13 warning Unexpected console statement no-console
|
||||
320:11 warning Unexpected console statement no-console
|
||||
345:7 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectViewView.vue
|
||||
387:9 warning Unexpected console statement no-console
|
||||
421:7 warning Unexpected console statement no-console
|
||||
457:7 warning Unexpected console statement no-console
|
||||
552:9 warning Unexpected console statement no-console
|
||||
554:11 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectsView.vue
|
||||
131:9 warning Unexpected console statement no-console
|
||||
144:7 warning Unexpected console statement no-console
|
||||
221:9 warning Unexpected console statement no-console
|
||||
237:7 warning Unexpected console statement no-console
|
||||
|
||||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/SeedBackupView.vue
|
||||
94:7 warning Unexpected console statement no-console
|
||||
|
||||
✖ 44 problems (0 errors, 44 warnings)
|
||||
|
||||
|
||||
You may use special comments to disable some warnings.
|
||||
Use // eslint-disable-next-line to ignore the next line.
|
||||
Use /* eslint-disable */ to ignore all warnings in a file.
|
||||
warning
|
||||
|
||||
/models/lupine_plant/textures/lambert2SG_baseColor.png is 3.75 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit.
|
||||
|
||||
warning
|
||||
|
||||
/models/lupine_plant/textures/lambert2SG_normal.png is 4.91 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit.
|
||||
|
||||
warning
|
||||
|
||||
asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
|
||||
This can impact web performance.
|
||||
Assets:
|
||||
js/project.44f30c9f.js (318 KiB)
|
||||
js/statistics.8a97010a.js (586 KiB)
|
||||
js/chunk-vendors.a4845bfb.js (411 KiB)
|
||||
js/705.f6a6ce2a.js (252 KiB)
|
||||
img/textures/leafy-autumn-forest-floor.jpg (705 KiB)
|
||||
models/lupine_plant/textures/lambert2SG_baseColor.png (3.58 MiB)
|
||||
models/lupine_plant/textures/lambert2SG_normal.png (4.69 MiB)
|
||||
|
||||
warning
|
||||
|
||||
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
|
||||
Entrypoints:
|
||||
app (447 KiB)
|
||||
js/chunk-vendors.a4845bfb.js
|
||||
css/app.8f21529c.css
|
||||
js/app.8833cebc.js
|
||||
|
||||
|
||||
File Size Gzipped
|
||||
|
||||
dist/js/statistics.8a97010a.js 585.72 KiB 148.80 KiB
|
||||
dist/js/chunk-vendors.a4845bfb.js 411.44 KiB 137.82 KiB
|
||||
dist/js/project.44f30c9f.js 317.61 KiB 78.67 KiB
|
||||
dist/js/705.f6a6ce2a.js 251.66 KiB 87.12 KiB
|
||||
dist/js/891.33615e4f.js 147.32 KiB 42.09 KiB
|
||||
dist/js/153.e2c8e249.js 146.26 KiB 42.21 KiB
|
||||
dist/js/820.13565d16.js 66.10 KiB 18.33 KiB
|
||||
dist/js/contact-qr.e170ec33.js 54.85 KiB 15.63 KiB
|
||||
dist/js/772.7b4c53a7.js 30.29 KiB 7.21 KiB
|
||||
dist/js/361.898a4525.js 27.40 KiB 8.19 KiB
|
||||
dist/js/account.77d86130.js 17.51 KiB 5.93 KiB
|
||||
dist/js/app.8833cebc.js 17.31 KiB 5.84 KiB
|
||||
dist/js/contacts.3fc90ff8.js 16.94 KiB 5.52 KiB
|
||||
dist/js/discover.24106939.js 15.30 KiB 5.22 KiB
|
||||
dist/js/536.3bb13201.js 15.23 KiB 4.84 KiB
|
||||
dist/workbox-5b385ed2.js 14.11 KiB 4.93 KiB
|
||||
dist/js/home.218b99dd.js 13.89 KiB 4.97 KiB
|
||||
dist/js/help.50d3117b.js 12.49 KiB 4.38 KiB
|
||||
dist/js/projects.417a6cb7.js 8.71 KiB 3.00 KiB
|
||||
dist/js/contact-amounts.a32b0ccd.js 8.44 KiB 3.25 KiB
|
||||
dist/js/229.120e09bf.js 7.99 KiB 2.72 KiB
|
||||
dist/js/identity-switcher.c7937333.js 7.44 KiB 2.52 KiB
|
||||
dist/js/new-edit-project.0552181b.js 7.36 KiB 3.11 KiB
|
||||
dist/js/300.dcaeb2a3.js 6.56 KiB 3.24 KiB
|
||||
dist/js/seed-backup.76a0f7b3.js 3.99 KiB 1.97 KiB
|
||||
dist/js/import-derive.c688d4b8.js 3.81 KiB 1.82 KiB
|
||||
dist/js/import-account.c3fa35fd.js 3.54 KiB 1.66 KiB
|
||||
dist/js/new-edit-account.bb763be2.js 3.39 KiB 1.51 KiB
|
||||
dist/js/431.5a6d64e0.js 3.38 KiB 2.56 KiB
|
||||
dist/service-worker.js 3.37 KiB 1.38 KiB
|
||||
dist/js/scan-contact.46be989a.js 2.79 KiB 1.18 KiB
|
||||
dist/js/start.091a7740.js 2.70 KiB 1.30 KiB
|
||||
dist/js/new-identifier.bb379420.js 2.12 KiB 1.18 KiB
|
||||
dist/js/93.b873dbbf.js 2.08 KiB 1.61 KiB
|
||||
dist/js/new-edit-commitment.9248d367.j 1.96 KiB 1.05 KiB
|
||||
s
|
||||
dist/js/confirm-contact.02004d1d.js 1.89 KiB 1.04 KiB
|
||||
dist/js/858.ae4c08ec.js 0.97 KiB 0.78 KiB
|
||||
dist/css/app.8f21529c.css 18.41 KiB 4.39 KiB
|
||||
dist/css/discover.73ee9bd3.css 14.77 KiB 6.25 KiB
|
||||
dist/css/new-edit-project.73ee9bd3.css 14.77 KiB 6.25 KiB
|
||||
dist/css/contacts.abb5e493.css 0.40 KiB 0.23 KiB
|
||||
dist/css/contact-amounts.5b26ccd4.css 0.31 KiB 0.20 KiB
|
||||
dist/css/home.828bc66e.css 0.25 KiB 0.19 KiB
|
||||
dist/css/project.828bc66e.css 0.25 KiB 0.19 KiB
|
||||
dist/css/statistics.828bc66e.css 0.25 KiB 0.19 KiB
|
||||
|
||||
Images and other types of assets omitted.
|
||||
Build at: 2023-09-07T10:06:43.972Z - Hash: 2b39fcd4d0e78263 - Time: 32016ms
|
||||
|
||||
DONE Build complete. The dist directory is ready to be deployed.
|
||||
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
|
||||
|
||||
67
src/App.vue
67
src/App.vue
@@ -157,7 +157,7 @@
|
||||
>
|
||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||
<p class="text-lg mb-4">
|
||||
Would you like to turn on notifications for this app?
|
||||
Would you like to <b>turn on</b> notifications for this app?
|
||||
</p>
|
||||
|
||||
<button
|
||||
@@ -181,6 +181,71 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'notification-mute'"
|
||||
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||
>
|
||||
<div
|
||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||
>
|
||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||
<p class="text-lg mb-4">Mute app notifications:</p>
|
||||
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
For 1 Hour
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
For 8 Hours
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
For 24 Hours
|
||||
</button>
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
Until I turn it back on
|
||||
</button>
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'notification-off'"
|
||||
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||
>
|
||||
<div
|
||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||
>
|
||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||
<p class="text-lg mb-4">
|
||||
Would you like to <b>turn off</b> notifications for this app?
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
Turn Off Notifications
|
||||
</button>
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
>
|
||||
Leave it On
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Notification>
|
||||
</div>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<div v-bind:class="computedAlertClassNames()">
|
||||
<button
|
||||
class="close-button bg-amber-400 w-8 leading-loose rounded-full absolute top-2 right-2"
|
||||
@click="onClickClose()"
|
||||
>
|
||||
<fa icon="xmark"></fa>
|
||||
</button>
|
||||
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
|
||||
<p>{{ alertMessage }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
|
||||
@Component
|
||||
export default class AlertMessage extends Vue {
|
||||
@Prop alertTitle = "";
|
||||
@Prop alertMessage = "";
|
||||
isAlertVisible = this.alertMessage;
|
||||
|
||||
public onClickClose() {
|
||||
this.isAlertVisible = false;
|
||||
}
|
||||
|
||||
public computedAlertClassNames() {
|
||||
return {
|
||||
hidden: !this.isAlertVisible,
|
||||
"dismissable-alert": true,
|
||||
"bg-amber-200": true,
|
||||
"p-5": true,
|
||||
rounded: true,
|
||||
"drop-shadow-lg": true,
|
||||
fixed: true,
|
||||
"top-3": true,
|
||||
"inset-x-3": true,
|
||||
"transition-transform": true,
|
||||
"ease-in": true,
|
||||
"duration-300": true,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped></style>
|
||||
@@ -5,13 +5,26 @@
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { toSvg } from "jdenticon";
|
||||
|
||||
const BLANK_CONFIG = {
|
||||
lightness: {
|
||||
color: [1.0, 1.0],
|
||||
grayscale: [1.0, 1.0],
|
||||
},
|
||||
saturation: {
|
||||
color: 0.0,
|
||||
grayscale: 0.0,
|
||||
},
|
||||
backColor: "#0000",
|
||||
};
|
||||
|
||||
@Component
|
||||
export default class EntityIcon extends Vue {
|
||||
@Prop entityId = "";
|
||||
@Prop iconSize = "";
|
||||
@Prop iconSize = 0;
|
||||
|
||||
generateIdenticon() {
|
||||
const svgString = toSvg(this.entityId, this.iconSize);
|
||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
||||
return svgString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 class="text-xl font-bold text-center mb-4">
|
||||
{{ message }} {{ giver?.name || "somebody not specified" }}
|
||||
{{ message }} {{ giver?.name || "somebody not named" }}
|
||||
</h1>
|
||||
<input
|
||||
type="text"
|
||||
@@ -51,19 +51,58 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { createAndSubmitGive, GiverInputInfo } from "@/libs/endorserServer";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class GiftedDialog extends Vue {
|
||||
@Prop message = "";
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
giver = null;
|
||||
@Prop message = "";
|
||||
@Prop projectId = "";
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
|
||||
giver?: GiverInputInfo;
|
||||
description = "";
|
||||
hours = "0";
|
||||
visible = false;
|
||||
|
||||
open(giver) {
|
||||
// giver: GiverInputInfo
|
||||
async created() {
|
||||
try {
|
||||
await db.open();
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
err.message ||
|
||||
"There was an error retrieving the latest sweet, sweet action.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
open(giver: GiverInputInfo) {
|
||||
this.giver = giver;
|
||||
this.visible = true;
|
||||
}
|
||||
@@ -80,27 +119,169 @@ export default class GiftedDialog extends Vue {
|
||||
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||
}
|
||||
|
||||
@Emit("dialog-result")
|
||||
confirm() {
|
||||
const result = {
|
||||
action: "confirm",
|
||||
giver: this.giver,
|
||||
hours: parseFloat(this.hours),
|
||||
description: this.description,
|
||||
};
|
||||
cancel() {
|
||||
this.close();
|
||||
this.description = "";
|
||||
this.giver = null;
|
||||
this.giver = undefined;
|
||||
this.hours = "0";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Emit("dialog-result")
|
||||
cancel() {
|
||||
const result = { action: "cancel" };
|
||||
async confirm() {
|
||||
this.close();
|
||||
return result;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
text: "Recording the give...",
|
||||
title: "",
|
||||
},
|
||||
1000,
|
||||
);
|
||||
// this is asynchronous, but we don't need to wait for it to complete
|
||||
this.recordGive(
|
||||
this.giver?.did as string | undefined,
|
||||
this.description,
|
||||
parseFloat(this.hours),
|
||||
).then(() => {
|
||||
this.description = "";
|
||||
this.giver = undefined;
|
||||
this.hours = "0";
|
||||
});
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
throw new Error(
|
||||
"Attempted to load Give records for DID ${activeDid} but no identity was found",
|
||||
);
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param giverDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
public async recordGive(
|
||||
giverDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
) {
|
||||
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,
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
console.log("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage || "There was an error creating the give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "That gift was recorded.",
|
||||
},
|
||||
10000,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.log("Error with give recordation caught:", error);
|
||||
const message =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
"There was an error recording the give.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isGiveCreationError(result: any) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getGiveCreationErrorMessage(result: any) {
|
||||
return (
|
||||
result.error?.userMessage ||
|
||||
result.error?.error ||
|
||||
result.response?.data?.error?.message
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* Generic strings that could be used throughout the app.
|
||||
*
|
||||
* See also ../libs/veramo/setup.ts
|
||||
*/
|
||||
export enum AppString {
|
||||
APP_NAME = "Kick-Start with Time",
|
||||
APP_NAME = "Time Safari",
|
||||
|
||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||
@@ -10,3 +12,13 @@ export enum AppString {
|
||||
|
||||
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
||||
}
|
||||
|
||||
/**
|
||||
* See notiwind package
|
||||
*/
|
||||
export interface NotificationIface {
|
||||
group: string;
|
||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
/**
|
||||
* Represents an account stored in the database.
|
||||
*/
|
||||
export type Account = {
|
||||
id?: number; // auto-generated by Dexie
|
||||
/**
|
||||
* Auto-generated ID by Dexie.
|
||||
*/
|
||||
id?: number;
|
||||
|
||||
/**
|
||||
* The date the account was created.
|
||||
*/
|
||||
dateCreated: string;
|
||||
|
||||
/**
|
||||
* The derivation path for the account.
|
||||
*/
|
||||
derivationPath: string;
|
||||
|
||||
/**
|
||||
* Decentralized Identifier (DID) for the account.
|
||||
*/
|
||||
did: string;
|
||||
// stringified JSON containing underlying key material of type IIdentifier
|
||||
// https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
|
||||
|
||||
/**
|
||||
* Stringified JSON containing underlying key material.
|
||||
* Based on the IIdentifier type from Veramo.
|
||||
* @see {@link https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts}
|
||||
*/
|
||||
identity: string;
|
||||
|
||||
/**
|
||||
* The public key in hexadecimal format.
|
||||
*/
|
||||
publicKeyHex: string;
|
||||
|
||||
/**
|
||||
* The mnemonic passphrase for the account.
|
||||
*/
|
||||
mnemonic: string;
|
||||
};
|
||||
|
||||
// mark encrypted field by starting with a $ character
|
||||
// see https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon
|
||||
/**
|
||||
* Schema for the accounts table in the database.
|
||||
* Fields starting with a $ character are encrypted.
|
||||
* @see {@link https://github.com/PVermeer/dexie-addon-suite-monorepo/tree/master/packages/dexie-encrypted-addon}
|
||||
*/
|
||||
export const AccountsSchema = {
|
||||
accounts:
|
||||
"++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",
|
||||
|
||||
@@ -7,5 +7,5 @@ export interface Contact {
|
||||
}
|
||||
|
||||
export const ContactsSchema = {
|
||||
contacts: "++did, name, publicKeyBase64, registered, seesMe",
|
||||
contacts: "&did, name, publicKeyBase64, registered, seesMe",
|
||||
};
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export type BoundingBox = {
|
||||
eastLong: number;
|
||||
maxLat: number;
|
||||
minLat: number;
|
||||
westLong: number;
|
||||
};
|
||||
|
||||
// a singleton
|
||||
export type Settings = {
|
||||
id: number; // there's only one entry: MASTER_SETTINGS_KEY
|
||||
@@ -5,8 +12,13 @@ export type Settings = {
|
||||
activeDid?: string;
|
||||
apiServer?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
isRegistered?: boolean;
|
||||
lastName?: string; // deprecated, pre v 0.1.3
|
||||
lastViewedClaimId?: string;
|
||||
searchBoxes?: Array<{
|
||||
name: string;
|
||||
bbox: BoundingBox;
|
||||
}>;
|
||||
showContactGivesInline?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
||||
import { getRandomBytesSync } from "ethereum-cryptography/random";
|
||||
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
|
||||
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
||||
@@ -7,7 +6,10 @@ import { HDNode } from "@ethersproject/hdnode";
|
||||
import * as didJwt from "did-jwt";
|
||||
import * as u8a from "uint8arrays";
|
||||
|
||||
export const DEFAULT_ROOT_DERIVATION_PATH = "m/76798669'/0'/0'/0'";
|
||||
import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer";
|
||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
||||
|
||||
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -150,3 +152,24 @@ export function fromJose(signature: string): {
|
||||
export function bytesToHex(b: Uint8Array): string {
|
||||
return u8a.toString(b, "base16");
|
||||
}
|
||||
|
||||
/**
|
||||
@return results of uportJwtPayload:
|
||||
{ iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } }
|
||||
|
||||
Note that similar code is also contained in time-safari
|
||||
*/
|
||||
export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
|
||||
let jwtText = jwtUrlText;
|
||||
const endorserContextLoc = jwtText.indexOf(ENDORSER_JWT_URL_LOCATION);
|
||||
if (endorserContextLoc > -1) {
|
||||
jwtText = jwtText.substring(
|
||||
endorserContextLoc + ENDORSER_JWT_URL_LOCATION.length,
|
||||
);
|
||||
}
|
||||
|
||||
// JWT format: { header, payload, signature, data }
|
||||
const jwt = didJwt.decodeJWT(jwtText);
|
||||
|
||||
return jwt.payload;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,12 @@ import { Axios, AxiosResponse } from "axios";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
|
||||
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||
// the object in RegisterAction claims
|
||||
export const SERVICE_ID = "endorser.ch";
|
||||
// the prefix for the contact URL
|
||||
export const CONTACT_URL_PREFIX = "https://endorser.ch";
|
||||
// the suffix for the contact URL
|
||||
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
|
||||
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
@@ -21,6 +26,13 @@ export interface GiverInputInfo {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface GiverOutputInfo {
|
||||
action: string;
|
||||
giver?: GiverInputInfo;
|
||||
description?: string;
|
||||
hours?: number;
|
||||
}
|
||||
|
||||
export interface ClaimResult {
|
||||
success: { claimId: string; handleId: string };
|
||||
error: { code: string; message: string };
|
||||
@@ -55,7 +67,30 @@ export interface GiveVerifiableCredential {
|
||||
fulfills?: { "@type": string; identifier: string };
|
||||
identifier?: string;
|
||||
object?: { amountOfThisGood: number; unitCode: string };
|
||||
recipient: { identifier: string };
|
||||
recipient?: { identifier: string };
|
||||
}
|
||||
|
||||
export interface PlanVerifiableCredential {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "PlanAction";
|
||||
name: string;
|
||||
description: string;
|
||||
identifier?: string;
|
||||
location?: {
|
||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlanServerRecord {
|
||||
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
||||
description: string;
|
||||
endTime?: string;
|
||||
issuerDid: string;
|
||||
handleId: string;
|
||||
locLat?: number;
|
||||
locLon?: number;
|
||||
startTime?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface RegisterVerifiableCredential {
|
||||
@@ -63,7 +98,7 @@ export interface RegisterVerifiableCredential {
|
||||
"@type": string;
|
||||
agent: { identifier: string };
|
||||
object: string;
|
||||
recipient: { identifier: string };
|
||||
participant: { identifier: string };
|
||||
}
|
||||
|
||||
export interface InternalError {
|
||||
@@ -75,36 +110,50 @@ export interface InternalError {
|
||||
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||
const HIDDEN_DID = "did:none:HIDDEN";
|
||||
|
||||
export function isHiddenDid(did) {
|
||||
export function isHiddenDid(did: string) {
|
||||
return did === HIDDEN_DID;
|
||||
}
|
||||
|
||||
/**
|
||||
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
||||
|
||||
Similar logic is found in endorser-mobile.
|
||||
**/
|
||||
export function didInfo(
|
||||
did: string,
|
||||
activeDid: string,
|
||||
allMyDids: Array<string>,
|
||||
contacts: Array<Contact>,
|
||||
allMyDids: string[],
|
||||
contacts: Contact[],
|
||||
): string {
|
||||
const myId: string | undefined = R.find(R.equals(did), allMyDids, did);
|
||||
if (myId) {
|
||||
return "You" + (myId !== activeDid ? " (Alt ID)" : "");
|
||||
} else {
|
||||
const contact: Contact | undefined = R.find((c) => c.did === did, contacts);
|
||||
if (contact) {
|
||||
return contact.name || "Someone Unnamed in Contacts";
|
||||
} else if (!did) {
|
||||
return "Unspecified Person";
|
||||
} else if (isHiddenDid(did)) {
|
||||
return "Someone Not In Network";
|
||||
} else {
|
||||
return "Someone Not In Contacts";
|
||||
}
|
||||
}
|
||||
if (!did) return "Someone Anonymous";
|
||||
|
||||
const myId = R.find(R.equals(did), allMyDids);
|
||||
if (myId) return `You${myId !== activeDid ? " (Alt ID)" : ""}`;
|
||||
|
||||
const contact = R.find((c) => c.did === did, contacts);
|
||||
return contact
|
||||
? contact.name || "Contact With No Name"
|
||||
: isHiddenDid(did)
|
||||
? "Someone Not In Network"
|
||||
: "Someone Not In Contacts";
|
||||
}
|
||||
|
||||
export interface ResultWithType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SuccessResult extends ResultWithType {
|
||||
type: "success";
|
||||
response: AxiosResponse<ClaimResult>;
|
||||
}
|
||||
|
||||
export interface ErrorResult {
|
||||
type: "error";
|
||||
error: InternalError;
|
||||
}
|
||||
|
||||
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
|
||||
|
||||
/**
|
||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||
*
|
||||
@@ -118,76 +167,83 @@ export async function createAndSubmitGive(
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
identity: IIdentifier,
|
||||
fromDid: string,
|
||||
toDid: string,
|
||||
description: string,
|
||||
hours: number,
|
||||
fromDid?: string,
|
||||
toDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
fulfillsProjectHandleId?: string,
|
||||
): Promise<AxiosResponse<ClaimResult> | InternalError> {
|
||||
// Make a claim
|
||||
const vcClaim: GiveVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "GiveAction",
|
||||
recipient: { identifier: toDid },
|
||||
};
|
||||
if (fromDid) {
|
||||
vcClaim.agent = { identifier: fromDid };
|
||||
}
|
||||
if (description) {
|
||||
vcClaim.description = description;
|
||||
}
|
||||
if (hours) {
|
||||
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
|
||||
}
|
||||
if (fulfillsProjectHandleId) {
|
||||
vcClaim.fulfills = {
|
||||
"@type": "PlanAction",
|
||||
identifier: fulfillsProjectHandleId,
|
||||
): Promise<CreateAndSubmitGiveResult> {
|
||||
try {
|
||||
const vcClaim: GiveVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "GiveAction",
|
||||
recipient: toDid ? { identifier: toDid } : undefined,
|
||||
agent: fromDid ? { identifier: fromDid } : undefined,
|
||||
description: description || undefined,
|
||||
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
|
||||
fulfills: fulfillsProjectHandleId
|
||||
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const vcPayload = {
|
||||
vc: {
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim,
|
||||
},
|
||||
};
|
||||
|
||||
// Create a signature using private key of identity
|
||||
const firstKey = identity.keys[0];
|
||||
const privateKeyHex = firstKey?.privateKeyHex;
|
||||
|
||||
if (!privateKeyHex) {
|
||||
throw {
|
||||
error: "No private key",
|
||||
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
|
||||
};
|
||||
}
|
||||
|
||||
const signer = await SimpleSigner(privateKeyHex);
|
||||
|
||||
// Create a JWT for the request
|
||||
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||
issuer: identity.did,
|
||||
signer,
|
||||
});
|
||||
|
||||
// Make the xhr request payload
|
||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||
const url = `${apiServer}/api/v2/claim`;
|
||||
const token = await accessToken(identity);
|
||||
|
||||
const response = await axios.post(url, payload, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return { type: "success", response };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage: string =
|
||||
error === null
|
||||
? "Null error"
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: typeof error === "object" && error !== null && "message" in error
|
||||
? (error as { message: string }).message
|
||||
: "Unknown error";
|
||||
|
||||
return {
|
||||
type: "error",
|
||||
error: {
|
||||
error: errorMessage,
|
||||
userMessage: "Failed to create and submit the claim.",
|
||||
},
|
||||
};
|
||||
}
|
||||
// Make a payload for the claim
|
||||
const vcPayload = {
|
||||
vc: {
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim,
|
||||
},
|
||||
};
|
||||
// Create a signature using private key of identity
|
||||
if (identity.keys[0].privateKeyHex == null) {
|
||||
return new Promise<InternalError>((resolve, reject) => {
|
||||
reject({
|
||||
error: "No private key",
|
||||
message:
|
||||
"Your identifier " +
|
||||
identity.did +
|
||||
" is not configured correctly. Use a different identifier.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
||||
const signer = await SimpleSigner(privateKeyHex);
|
||||
const alg = undefined;
|
||||
// Create a JWT for the request
|
||||
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||
alg: alg,
|
||||
issuer: identity.did,
|
||||
signer: signer,
|
||||
});
|
||||
|
||||
// Make the xhr request payload
|
||||
|
||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||
const url = apiServer + "/api/v2/claim";
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + token,
|
||||
};
|
||||
|
||||
return axios.post(url, payload, { headers });
|
||||
}
|
||||
|
||||
// from https://stackoverflow.com/a/175787/845494
|
||||
|
||||
@@ -1,151 +1,7 @@
|
||||
// Created from the setup in https://veramo.io/docs/guides/react_native
|
||||
// see also ../constants/app.ts and
|
||||
|
||||
// Core interfaces
|
||||
/* import {
|
||||
createAgent,
|
||||
IDIDManager,
|
||||
IResolver,
|
||||
IDataStore,
|
||||
IKeyManager,
|
||||
} from "@veramo/core";
|
||||
*/
|
||||
// Core identity manager plugin
|
||||
//import { DIDManager } from "@veramo/did-manager";
|
||||
|
||||
// Ethr did identity provider
|
||||
//import { EthrDIDProvider } from "@veramo/did-provider-ethr";
|
||||
|
||||
// Core key manager plugin
|
||||
//import { KeyManager } from "@veramo/key-manager";
|
||||
|
||||
// Custom key management system for RN
|
||||
//import { KeyManagementSystem } from '@veramo/kms-local-react-native'
|
||||
|
||||
// Custom resolver
|
||||
// Custom resolvers
|
||||
//import { DIDResolverPlugin } from "@veramo/did-resolver";
|
||||
/* import { Resolver } from "did-resolver";
|
||||
import { getResolver as ethrDidResolver } from "ethr-did-resolver";
|
||||
import { getResolver as webDidResolver } from "web-did-resolver";
|
||||
*/
|
||||
// for VCs and VPs https://veramo.io/docs/api/credential-w3c
|
||||
//import { CredentialIssuer } from '@veramo/credential-w3c'
|
||||
|
||||
// Storage plugin using TypeOrm
|
||||
/* import {
|
||||
Entities,
|
||||
KeyStore,
|
||||
DIDStore,
|
||||
IDataStoreORM,
|
||||
} from "@veramo/data-store";
|
||||
*/
|
||||
// TypeORM is installed with @veramo/typeorm
|
||||
//import { createConnection } from 'typeorm'
|
||||
|
||||
//import * as R from "ramda";
|
||||
|
||||
/*
|
||||
import { Contact } from '../entity/contact'
|
||||
import { Settings } from '../entity/settings'
|
||||
import { PrivateData } from '../entity/privateData'
|
||||
|
||||
import { Initial1616938713828 } from '../migration/1616938713828-initial'
|
||||
import { SettingsContacts1616967972293 } from '../migration/1616967972293-settings-contacts'
|
||||
import { EncryptedSeed1637856484788 } from '../migration/1637856484788-EncryptedSeed'
|
||||
import { HomeScreenConfig1639947962124 } from '../migration/1639947962124-HomeScreenConfig'
|
||||
import { HandlePublicKeys1652142819353 } from '../migration/1652142819353-HandlePublicKeys'
|
||||
import { LastClaimsSeen1656811846836 } from '../migration/1656811846836-LastClaimsSeen'
|
||||
import { ContactRegistered1662256903367 }from '../migration/1662256903367-ContactRegistered'
|
||||
import { PrivateData1663080623479 } from '../migration/1663080623479-PrivateData'
|
||||
|
||||
const ALL_ENTITIES = Entities.concat([Contact, Settings, PrivateData])
|
||||
|
||||
// Create react native DB connection configured by ormconfig.js
|
||||
|
||||
export const dbConnection = createConnection({
|
||||
database: 'endorser-mobile.sqlite',
|
||||
entities: ALL_ENTITIES,
|
||||
location: 'default',
|
||||
logging: ['error', 'info', 'warn'],
|
||||
migrations: [ Initial1616938713828, SettingsContacts1616967972293, EncryptedSeed1637856484788, HomeScreenConfig1639947962124, HandlePublicKeys1652142819353, LastClaimsSeen1656811846836, ContactRegistered1662256903367, PrivateData1663080623479 ],
|
||||
migrationsRun: true,
|
||||
type: 'react-native',
|
||||
})
|
||||
*/
|
||||
function didProviderName(netName: string) {
|
||||
return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName);
|
||||
}
|
||||
|
||||
//const NETWORK_NAMES = ["mainnet", "rinkeby"];
|
||||
|
||||
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
|
||||
|
||||
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
|
||||
DEFAULT_DID_PROVIDER_NETWORK_NAME,
|
||||
);
|
||||
|
||||
export const HANDY_APP = false;
|
||||
|
||||
// this is used as the object in RegisterAction claims
|
||||
export const SERVICE_ID = "endorser.ch";
|
||||
|
||||
//const INFURA_PROJECT_ID = "INFURA_PROJECT_ID";
|
||||
/*
|
||||
const providers = {}
|
||||
NETWORK_NAMES.forEach((networkName) => {
|
||||
providers[didProviderName(networkName)] = new EthrDIDProvider({
|
||||
defaultKms: 'local',
|
||||
network: networkName,
|
||||
rpcUrl: 'https://' + networkName + '.infura.io/v3/' + INFURA_PROJECT_ID,
|
||||
gas: 1000001,
|
||||
ttl: 60 * 60 * 24 * 30 * 12 + 1,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const didManager = new DIDManager({
|
||||
store: new DIDStore(dbConnection),
|
||||
defaultProvider: DEFAULT_DID_PROVIDER_NAME,
|
||||
providers: providers,
|
||||
})
|
||||
*/
|
||||
|
||||
/* const basicDidResolvers = NETWORK_NAMES.map((networkName) => [
|
||||
networkName,
|
||||
new Resolver({
|
||||
ethr: ethrDidResolver({
|
||||
networks: [
|
||||
{
|
||||
name: networkName,
|
||||
rpcUrl:
|
||||
"https://" + networkName + ".infura.io/v3/" + INFURA_PROJECT_ID,
|
||||
},
|
||||
],
|
||||
}).ethr,
|
||||
web: webDidResolver().web,
|
||||
}),
|
||||
]);
|
||||
|
||||
const basicResolverMap = R.fromPairs(basicDidResolvers)
|
||||
|
||||
export const DEFAULT_BASIC_RESOLVER = basicResolverMap[DEFAULT_DID_PROVIDER_NETWORK_NAME]
|
||||
|
||||
const agentDidResolvers = NETWORK_NAMES.map((networkName) => {
|
||||
return new DIDResolverPlugin({
|
||||
resolver: basicResolverMap[networkName],
|
||||
})
|
||||
})
|
||||
|
||||
let allPlugins = [
|
||||
new CredentialIssuer(),
|
||||
new KeyManager({
|
||||
store: new KeyStore(dbConnection),
|
||||
kms: {
|
||||
local: new KeyManagementSystem(),
|
||||
},
|
||||
}),
|
||||
didManager,
|
||||
].concat(agentDidResolvers)
|
||||
*/
|
||||
|
||||
//export const agent = createAgent<IDIDManager & IKeyManager & IDataStore & IDataStoreORM & IResolver>({ plugins: allPlugins })
|
||||
export const DEFAULT_DID_PROVIDER_NAME = didProviderName("mainnet");
|
||||
|
||||
@@ -13,6 +13,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faBan,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faChevronLeft,
|
||||
@@ -59,6 +60,7 @@ import {
|
||||
library.add(
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faBan,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faChevronLeft,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||
import { accountsDB } from "@/db";
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
NavigationGuardNext,
|
||||
RouteLocationNormalized,
|
||||
RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import { accountsDB } from "@/db/index";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -7,7 +13,11 @@ import { accountsDB } from "@/db";
|
||||
* @param from :RouteLocationNormalized
|
||||
* @param next :NavigationGuardNext
|
||||
*/
|
||||
const enterOrStart = async (to, from, next) => {
|
||||
const enterOrStart = async (
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
) => {
|
||||
await accountsDB.open();
|
||||
const num_accounts = await accountsDB.accounts.count();
|
||||
if (num_accounts > 0) {
|
||||
@@ -23,7 +33,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "home",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
|
||||
beforeEnter: enterOrStart,
|
||||
},
|
||||
{
|
||||
path: "/account",
|
||||
@@ -48,6 +57,14 @@ const routes: Array<RouteRecordRaw> = [
|
||||
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/contact-gives",
|
||||
name: "contact-gives",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/contact-qr",
|
||||
name: "contact-qr",
|
||||
@@ -61,7 +78,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "contacts",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
|
||||
beforeEnter: enterOrStart,
|
||||
},
|
||||
{
|
||||
path: "/scan-contact",
|
||||
@@ -175,12 +191,10 @@ const routes: Array<RouteRecordRaw> = [
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/contact-gives",
|
||||
name: "contact-gives",
|
||||
path: "/test",
|
||||
name: "test",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "statistics" */ "../views/ContactGiftingView.vue"
|
||||
),
|
||||
import(/* webpackChunkName: "test" */ "../views/TestView.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -190,7 +204,12 @@ const router = createRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
const errorHandler = (error, to, from) => {
|
||||
const errorHandler = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any,
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
) => {
|
||||
// Handle the error here
|
||||
console.error("Caught in top level error handler:", error, to, from);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
||||
import * as didJwt from "did-jwt";
|
||||
import { AppString } from "@/constants/app";
|
||||
import { db } from "../db";
|
||||
import { SERVICE_ID } from "../libs/veramo/setup";
|
||||
import { SERVICE_ID } from "../libs/endorserServer";
|
||||
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
|
||||
|
||||
@@ -52,7 +52,17 @@
|
||||
|
||||
<!-- Identity Details -->
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<h2 class="text-xl font-semibold mb-2">{{ firstName }} {{ lastName }}</h2>
|
||||
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
||||
{{ givenName }}
|
||||
</h2>
|
||||
<span v-else>
|
||||
<router-link
|
||||
:to="{ name: 'new-edit-account' }"
|
||||
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
||||
>
|
||||
(set name)
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">ID</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
@@ -67,70 +77,75 @@
|
||||
</button>
|
||||
<span v-show="showDidCopy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
<code class="truncate">{{ publicBase64 }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showB64Copy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
<code class="truncate">{{ publicHex }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showPubCopy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
<code class="truncate">{{ derivationPath }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(derivationPath, () => (showDerCopy = !showDerCopy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showDerCopy">Copied!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
:to="{ name: 'new-edit-account' }"
|
||||
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||
class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
|
||||
>
|
||||
Edit Identity
|
||||
</router-link>
|
||||
<button
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-permission',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="block w-full text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
|
||||
>
|
||||
Turn on Notifications
|
||||
</button>
|
||||
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||
<label
|
||||
for="toggleNotifications"
|
||||
class="flex items-center cursor-pointer"
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-permission',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- label -->
|
||||
<div>App Notifications</div>
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input type="checkbox" name="toggleNotifications" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
for="toggleMuteNotifications"
|
||||
class="flex items-center cursor-pointer mt-4"
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-mute',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- label -->
|
||||
<div>Mute Notifications</div>
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
name="toggleMuteNotifications"
|
||||
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>
|
||||
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||
|
||||
@@ -145,45 +160,117 @@
|
||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
||||
@click="exportDatabase()"
|
||||
>
|
||||
Download Settings & Contacts (excluding Identifier Data)
|
||||
Download Settings & Contacts
|
||||
<br />
|
||||
(excluding Identifier Data)
|
||||
</a>
|
||||
<a ref="downloadLink" />
|
||||
|
||||
<!-- QR code popup -->
|
||||
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
|
||||
<div class="text-slate-500 text-center">
|
||||
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
||||
<div v-if="activeDid" 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>
|
||||
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
|
||||
|
||||
<button
|
||||
value="cancel"
|
||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
<button
|
||||
value="cancel"
|
||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</dialog>
|
||||
<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>
|
||||
|
||||
<!-- id used by puppeteer test script -->
|
||||
<h3
|
||||
id="advanced"
|
||||
class="text-sm uppercase font-semibold mb-3"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
>
|
||||
Advanced
|
||||
</h3>
|
||||
|
||||
<div v-if="showAdvanced">
|
||||
<!-- Deep Identity Details -->
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
|
||||
Deep Identity Details
|
||||
</h2>
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
||||
<div
|
||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
||||
>
|
||||
<code class="truncate">{{ publicBase64 }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showB64Copy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
||||
<div
|
||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
||||
>
|
||||
<code class="truncate">{{ publicHex }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showPubCopy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
||||
<div
|
||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
||||
>
|
||||
<code class="truncate">{{ derivationPath }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(
|
||||
derivationPath,
|
||||
() => (showDerCopy = !showDerCopy),
|
||||
)
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showDerCopy">Copied!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label
|
||||
for="toggleShowAmounts"
|
||||
class="flex items-center cursor-pointer mb-6"
|
||||
class="flex items-center cursor-pointer py-2"
|
||||
@click="handleChange"
|
||||
>
|
||||
<!-- label -->
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||
Show amounts given with contacts
|
||||
</h2>
|
||||
<!-- toggle -->
|
||||
<div class="relative">
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -198,51 +285,31 @@
|
||||
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 class="text-blue-500">
|
||||
<!-- id used by puppeteer test script -->
|
||||
<router-link
|
||||
id="switch-identity-link"
|
||||
:to="{ name: 'identity-switcher' }"
|
||||
class="block text-center"
|
||||
>
|
||||
Switch Identity / No Identity
|
||||
</router-link>
|
||||
</button>
|
||||
<!-- 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">
|
||||
<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-2"
|
||||
>
|
||||
Switch Identity / No Identity
|
||||
</router-link>
|
||||
<button class="text-blue-500">
|
||||
<router-link :to="{ name: 'statistics' }" class="block text-center">
|
||||
See Achievements & Statistics
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex py-2">
|
||||
Claim Server
|
||||
<div class="flex py-4">
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
@@ -274,52 +341,53 @@
|
||||
Use Local
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="text-blue-500">
|
||||
<router-link
|
||||
:to="{ name: 'statistics' }"
|
||||
class="block text-center py-3"
|
||||
>
|
||||
See Achievements & Statistics
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosError } from "axios";
|
||||
import "dexie-export-import";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { AppString } from "@/constants/app";
|
||||
import { db, accountsDB } from "@/db";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { AxiosError } from "axios/index";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Buffer = require("buffer/").Buffer;
|
||||
|
||||
@Component({ components: { AlertMessage, QuickNav } })
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface IAccount {
|
||||
did: string;
|
||||
publicKeyHex: string;
|
||||
privateHex?: string;
|
||||
derivationPath: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class AccountViewView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
Constants = AppString;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
apiServerInput = "";
|
||||
derivationPath = "";
|
||||
firstName = "";
|
||||
lastName = "";
|
||||
givenName = "";
|
||||
isRegistered = false;
|
||||
numAccounts = 0;
|
||||
publicHex = "";
|
||||
publicBase64 = "";
|
||||
@@ -334,30 +402,61 @@ export default class AccountViewView extends Vue {
|
||||
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 getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
||||
try {
|
||||
// Open the accounts database
|
||||
await accountsDB.open();
|
||||
} catch (error) {
|
||||
console.error("Failed to open accounts database:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
let account: { identity?: string } | undefined;
|
||||
|
||||
try {
|
||||
// Search for the account with the matching DID (decentralized identifier)
|
||||
account = await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
} catch (error) {
|
||||
console.error("Failed to find account:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return parsed identity or null if not found
|
||||
return JSON.parse((account?.identity as string) || "null");
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + token,
|
||||
};
|
||||
return headers;
|
||||
/**
|
||||
* Asynchronously retrieves headers for HTTP requests.
|
||||
*
|
||||
* @param {IIdentifier} identity - The identity object for which to generate the headers.
|
||||
* @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
|
||||
*
|
||||
* @throws Will throw an error if unable to generate an access token.
|
||||
*/
|
||||
public async getHeaders(
|
||||
identity: IIdentifier,
|
||||
): Promise<Record<string, string>> {
|
||||
try {
|
||||
const token = await accessToken(identity);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
return headers;
|
||||
} catch (error) {
|
||||
console.error("Failed to get headers:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
||||
doCopyTwoSecRedo(text, fn) {
|
||||
doCopyTwoSecRedo(text: string, fn: () => void) {
|
||||
fn();
|
||||
useClipboard()
|
||||
.copy(text)
|
||||
@@ -378,59 +477,94 @@ export default class AccountViewView extends Vue {
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Async function executed when the component is created.
|
||||
* Initializes the component's state with values from the database,
|
||||
* handles identity-related tasks, and checks limitations.
|
||||
*
|
||||
* @throws Will display specific messages to the user based on different errors.
|
||||
*/
|
||||
async created() {
|
||||
// Uncomment this to register this user on the test server.
|
||||
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
|
||||
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
|
||||
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
|
||||
//testServerRegisterUser();
|
||||
|
||||
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 || "";
|
||||
this.lastName = settings?.lastName || "";
|
||||
this.showContactGives = !!settings?.showContactGivesInline;
|
||||
|
||||
// Initialize component state with values from the database or defaults
|
||||
this.initializeState(settings);
|
||||
|
||||
// Get and process the identity
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
|
||||
if (identity) {
|
||||
this.publicHex = identity.keys[0].publicKeyHex;
|
||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString(
|
||||
"base64",
|
||||
);
|
||||
this.derivationPath = identity.keys[0].meta.derivationPath;
|
||||
this.processIdentity(identity);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
this.handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: identity.did,
|
||||
});
|
||||
this.checkLimitsFor(identity);
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err.message ===
|
||||
/**
|
||||
* Initializes component state with values from the database or defaults.
|
||||
* @param {SettingsType} settings - Object containing settings from the database.
|
||||
*/
|
||||
initializeState(settings: Settings | undefined) {
|
||||
this.activeDid = (settings?.activeDid as string) || "";
|
||||
this.apiServer = (settings?.apiServer as string) || "";
|
||||
this.apiServerInput = (settings?.apiServer as string) || "";
|
||||
this.givenName =
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.showContactGives = !!settings?.showContactGivesInline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the identity and updates the component's state.
|
||||
* @param {IdentityType} identity - Object containing identity information.
|
||||
*/
|
||||
processIdentity(identity: IIdentifier) {
|
||||
if (
|
||||
identity &&
|
||||
identity.keys &&
|
||||
identity.keys.length > 0 &&
|
||||
identity.keys[0].meta
|
||||
) {
|
||||
this.publicHex = identity.keys[0].publicKeyHex;
|
||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||
this.derivationPath = identity.keys[0].meta.derivationPath as string;
|
||||
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: identity.did,
|
||||
});
|
||||
this.checkLimitsFor(identity);
|
||||
} else {
|
||||
// Handle the case where any of these are null or undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors and updates the component's state accordingly.
|
||||
* @param {Error} err - The error object.
|
||||
*/
|
||||
handleError(err: unknown) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
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,
|
||||
);
|
||||
}
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,41 +591,96 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously exports the database into a downloadable JSON file.
|
||||
*
|
||||
* @throws Will notify the user if there is an export error.
|
||||
*/
|
||||
public async exportDatabase() {
|
||||
try {
|
||||
const blob = await db.export({ prettyJson: true });
|
||||
const url = URL.createObjectURL(blob);
|
||||
// Generate the blob from the database
|
||||
const blob = await this.generateDatabaseBlob();
|
||||
|
||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||
downloadAnchor.href = url;
|
||||
downloadAnchor.download = db.name + "-backup.json";
|
||||
downloadAnchor.click();
|
||||
// Create a temporary URL for the blob
|
||||
const url = this.createBlobURL(blob);
|
||||
|
||||
// Trigger the download
|
||||
this.downloadDatabaseBackup(url);
|
||||
|
||||
// Revoke the temporary URL
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Download Started",
|
||||
text: "See your downloads directory for the backup.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
// Notify the user that the download has started
|
||||
this.notifyDownloadStarted();
|
||||
} catch (error) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Export Error",
|
||||
text: "See console logs for more info.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error("Export Error:", error);
|
||||
this.handleExportError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a blob object representing the database.
|
||||
*
|
||||
* @returns {Promise<Blob>} The generated blob object.
|
||||
*/
|
||||
private async generateDatabaseBlob(): Promise<Blob> {
|
||||
return await db.export({ prettyJson: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary URL for a blob object.
|
||||
*
|
||||
* @param {Blob} blob - The blob object.
|
||||
* @returns {string} The temporary URL for the blob.
|
||||
*/
|
||||
private createBlobURL(blob: Blob): string {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the download of the database backup.
|
||||
*
|
||||
* @param {string} url - The temporary URL for the blob.
|
||||
*/
|
||||
private downloadDatabaseBackup(url: string) {
|
||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||
downloadAnchor.href = url;
|
||||
downloadAnchor.download = `${db.name}-backup.json`;
|
||||
downloadAnchor.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the user that the download has started.
|
||||
*/
|
||||
private notifyDownloadStarted() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Download Started",
|
||||
text: "See your downloads directory for the backup.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors during the database export process.
|
||||
*
|
||||
* @param {Error} error - The error object.
|
||||
*/
|
||||
private handleExportError(error: unknown) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Export Error",
|
||||
text: "See console logs for more info.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error("Export Error:", error);
|
||||
}
|
||||
|
||||
async checkLimits() {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
if (identity) {
|
||||
@@ -499,71 +688,146 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async checkLimitsFor(identity: IIdentifier) {
|
||||
/**
|
||||
* Asynchronously checks rate limits for the given identity.
|
||||
*
|
||||
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
|
||||
*/
|
||||
public async checkLimitsFor(identity: IIdentifier) {
|
||||
this.loadingLimits = true;
|
||||
this.limitsMessage = "";
|
||||
|
||||
try {
|
||||
const url = this.apiServer + "/api/report/rateLimits";
|
||||
const headers = await this.getHeaders(identity);
|
||||
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
// axios throws an exception on a 400
|
||||
const resp = await this.fetchRateLimits(identity);
|
||||
if (resp.status === 200) {
|
||||
this.limits = resp.data;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
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);
|
||||
|
||||
const data: ErrorResponse | undefined =
|
||||
serverError.response && serverError.response.data;
|
||||
if (data && data.error && data.error.message) {
|
||||
this.limitsMessage = data.error.message;
|
||||
} else {
|
||||
this.limitsMessage = "Bad server response.";
|
||||
if (!this.isRegistered) {
|
||||
// the user is not known to be registered, but they are so let's record it
|
||||
try {
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
isRegistered: true,
|
||||
});
|
||||
this.isRegistered = true;
|
||||
} catch (err) {
|
||||
console.log("Got an error updating settings:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Update Error",
|
||||
text: "Unable to update your settings. Check claim limits again.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleRateLimitsError(error);
|
||||
}
|
||||
|
||||
this.loadingLimits = false;
|
||||
}
|
||||
|
||||
async switchAccount(accountNum: number) {
|
||||
// 0 means none
|
||||
if (accountNum === 0) {
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: undefined,
|
||||
});
|
||||
this.activeDid = "";
|
||||
this.derivationPath = "";
|
||||
this.publicHex = "";
|
||||
this.publicBase64 = "";
|
||||
/**
|
||||
* Fetches rate limits from the server.
|
||||
*
|
||||
* @param {IIdentifier} identity - The identity object to check rate limits for.
|
||||
* @returns {Promise<AxiosResponse>} The Axios response object.
|
||||
*/
|
||||
private async fetchRateLimits(identity: IIdentifier) {
|
||||
const url = `${this.apiServer}/api/report/rateLimits`;
|
||||
const headers = await this.getHeaders(identity);
|
||||
return await this.axios.get(url, { headers });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors that occur while fetching rate limits.
|
||||
*
|
||||
* @param {AxiosError | Error} error - The error object.
|
||||
*/
|
||||
private handleRateLimitsError(error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
const data = error.response?.data as ErrorResponse;
|
||||
this.limitsMessage =
|
||||
(data?.error?.message as string) || "Bad server response.";
|
||||
console.log(
|
||||
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
|
||||
this.limitsMessage,
|
||||
//error,
|
||||
);
|
||||
} else if (
|
||||
error instanceof Error &&
|
||||
error.message ===
|
||||
"Attempted to load Give records with no identity available."
|
||||
) {
|
||||
this.limitsMessage = "No identity.";
|
||||
} else {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account = accounts[accountNum - 1];
|
||||
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: account.did,
|
||||
});
|
||||
|
||||
this.activeDid = account.did;
|
||||
this.derivationPath = account.derivationPath;
|
||||
this.publicHex = account.publicKeyHex;
|
||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||
// Handle other unknown errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously switches the active account based on the provided account number.
|
||||
*
|
||||
* @param {number} accountNum - The account number to switch to. 0 means none.
|
||||
*/
|
||||
public async switchAccount(accountNum: number) {
|
||||
await db.open(); // Assumes db needs to be open for both cases
|
||||
|
||||
if (accountNum === 0) {
|
||||
this.switchToNoAccount();
|
||||
} else {
|
||||
await this.switchToAccountNumber(accountNum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to no active account and clears relevant properties.
|
||||
*/
|
||||
private async switchToNoAccount() {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: undefined });
|
||||
this.clearActiveAccountProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears properties related to the active account.
|
||||
*/
|
||||
private clearActiveAccountProperties() {
|
||||
this.activeDid = "";
|
||||
this.derivationPath = "";
|
||||
this.publicHex = "";
|
||||
this.publicBase64 = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to an account based on its number in the list.
|
||||
*
|
||||
* @param {number} accountNum - The account number to switch to.
|
||||
*/
|
||||
private async switchToAccountNumber(accountNum: number) {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account = accounts[accountNum - 1];
|
||||
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
|
||||
|
||||
this.updateActiveAccountProperties(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates properties related to the active account.
|
||||
*
|
||||
* @param {AccountType} account - The account object.
|
||||
*/
|
||||
private updateActiveAccountProperties(account: IAccount) {
|
||||
this.activeDid = account.did;
|
||||
this.derivationPath = account.derivationPath;
|
||||
this.publicHex = account.publicKeyHex;
|
||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||
}
|
||||
|
||||
public showContactGivesClassNames() {
|
||||
return {
|
||||
"bg-slate-900": !this.showContactGives,
|
||||
@@ -579,7 +843,7 @@ export default class AccountViewView extends Vue {
|
||||
this.apiServer = this.apiServerInput;
|
||||
}
|
||||
|
||||
setApiServerInput(value) {
|
||||
setApiServerInput(value: string) {
|
||||
this.apiServerInput = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +90,6 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -101,7 +97,7 @@
|
||||
import * as R from "ramda";
|
||||
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||
@@ -113,17 +109,24 @@ import {
|
||||
} from "@/libs/endorserServer";
|
||||
import * as didJwt from "did-jwt";
|
||||
import { AxiosError } from "axios";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
|
||||
@Component({ components: { AlertMessage, QuickNav } })
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class ContactsView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
contact: Contact | null = null;
|
||||
giveRecords: Array<GiveServerRecord> = [];
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
numAccounts = 0;
|
||||
|
||||
async beforeCreate() {
|
||||
@@ -131,7 +134,7 @@ export default class ContactsView extends Vue {
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -147,7 +150,7 @@ export default class ContactsView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -169,7 +172,8 @@ export default class ContactsView extends Vue {
|
||||
if (this.activeDid && this.contact) {
|
||||
this.loadGives(this.activeDid, this.contact);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -187,7 +191,7 @@ export default class ContactsView extends Vue {
|
||||
async loadGives(activeDid: string, contact: Contact) {
|
||||
try {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
let result = [];
|
||||
let result: Array<GiveServerRecord> = [];
|
||||
const url =
|
||||
this.apiServer +
|
||||
"/api/v2/report/gives?agentDid=" +
|
||||
|
||||
@@ -16,19 +16,15 @@
|
||||
</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"
|
||||
:entityId="null"
|
||||
:iconSize="32"
|
||||
class="opacity-50 inline-block align-middle border border-dashed border-slate-400 bg-slate-200 rounded-md mr-1"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
></EntityIcon>
|
||||
Anonymous
|
||||
</span>
|
||||
@@ -70,59 +66,53 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<GiftedDialog
|
||||
ref="customDialog"
|
||||
@dialog-result="handleDialogResult"
|
||||
message="Received from"
|
||||
>
|
||||
</GiftedDialog>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
|
||||
</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 { db, accountsDB } from "@/db/index";
|
||||
import { Account, AccountsSchema } from "@/db/tables/accounts";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { createAndSubmitGive } from "@/libs/endorserServer";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import { GiverInputInfo } from "@/libs/endorserServer";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class HomeView extends Vue {
|
||||
export default class ContactGiftingView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
allAccounts: Array<Account> = [];
|
||||
allContacts: Array<Contact> = [];
|
||||
apiServer = "";
|
||||
isHiddenSpinner = true;
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
accounts: AccountsSchema;
|
||||
accounts: typeof AccountsSchema;
|
||||
numAccounts = 0;
|
||||
|
||||
async beforeCreate() {
|
||||
accountsDB.open();
|
||||
this.accounts = accountsDB.accounts;
|
||||
this.numAccounts = await this.accounts.count();
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -133,7 +123,7 @@ export default class HomeView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -144,23 +134,20 @@ export default class HomeView extends Vue {
|
||||
|
||||
async created() {
|
||||
try {
|
||||
await accountsDB.open();
|
||||
this.allAccounts = await accountsDB.accounts.toArray();
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||
this.updateAllFeed();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
err.userMessage ||
|
||||
err.message ||
|
||||
"There was an error retrieving the latest sweet, sweet action.",
|
||||
},
|
||||
-1,
|
||||
@@ -168,144 +155,8 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
openDialog(giver: GiverInputInfo) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Heading -->
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||
Your Contact Info
|
||||
</h1>
|
||||
|
||||
@@ -17,35 +17,54 @@
|
||||
:dotsOptions="{ type: 'square' }"
|
||||
class="flex justify-center"
|
||||
/>
|
||||
|
||||
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
|
||||
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { QrcodeStream } from "vue-qrcode-reader";
|
||||
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import * as R from "ramda";
|
||||
import { SimpleSigner } from "@/libs/crypto";
|
||||
import * as didJwt from "did-jwt";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
import {
|
||||
CONTACT_URL_PREFIX,
|
||||
ENDORSER_JWT_URL_LOCATION,
|
||||
} from "@/libs/endorserServer";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Buffer = require("buffer/").Buffer;
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
QrcodeStream,
|
||||
QRCodeVue3,
|
||||
QuickNav,
|
||||
},
|
||||
})
|
||||
export default class ContactQRScanShow extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
qrValue = "";
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account: Account | undefined = R.find(
|
||||
@@ -89,7 +108,9 @@ export default class ContactQRScanShow extends Vue {
|
||||
iat: Date.now(),
|
||||
iss: this.activeDid,
|
||||
own: {
|
||||
name: (settings?.firstName || "") + " " + (settings?.lastName || ""),
|
||||
name:
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
||||
publicEncKey,
|
||||
},
|
||||
};
|
||||
@@ -103,9 +124,47 @@ export default class ContactQRScanShow extends Vue {
|
||||
issuer: identity.did,
|
||||
signer: signer,
|
||||
});
|
||||
const viewPrefix = "https://endorser.ch/contact?jwt=";
|
||||
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
|
||||
this.qrValue = viewPrefix + vcJwt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param content is the result of a QR scan, an array with one item with a rawValue property
|
||||
*/
|
||||
// Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onScanDetect(content: any) {
|
||||
if (content[0]?.rawValue) {
|
||||
console.log("onDetect", content[0].rawValue);
|
||||
localStorage.setItem("contactEndorserUrl", content[0].rawValue);
|
||||
this.$router.push({ name: "contacts" });
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Invalid Contact QR Code",
|
||||
text: "No QR code detected with contact information.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onScanError(error: any) {
|
||||
console.log("Scan was invalid:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Invalid Scan",
|
||||
text: "The scan was invalid.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
|
||||
<!-- New Contact -->
|
||||
<div class="mb-4 flex">
|
||||
<span class="self-center bg-slate-500 text-white px-1.5 py-1 rounded-md">
|
||||
<router-link :to="{ name: 'contact-qr' }">
|
||||
<fa icon="qrcode" class="fa-fw" />
|
||||
</router-link>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="DID, Name, Public Key"
|
||||
@@ -88,6 +93,16 @@
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||
></EntityIcon>
|
||||
{{ contact.name || "(no name)" }}
|
||||
<button
|
||||
class="text-sm uppercase bg-slate-500 text-white px-1 rounded-md"
|
||||
@click="
|
||||
contactEdit = contact;
|
||||
contactNewName = contact.name;
|
||||
"
|
||||
title="Edit"
|
||||
>
|
||||
<fa icon="pen" class="fa-fw" />
|
||||
</button>
|
||||
</h2>
|
||||
<div class="text-sm truncate">{{ contact.did }}</div>
|
||||
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
||||
@@ -121,19 +136,21 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="contact.registered"
|
||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||
title="Registered"
|
||||
>
|
||||
<fa icon="person-circle-check" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="register(contact)"
|
||||
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" />
|
||||
<fa
|
||||
v-if="contact.registered"
|
||||
icon="person-circle-check"
|
||||
class="fa-fw"
|
||||
title="Registered"
|
||||
/>
|
||||
<fa
|
||||
v-else
|
||||
icon="person-circle-question"
|
||||
class="fa-fw"
|
||||
title="Registration Unknown"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -202,10 +219,31 @@
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>This identity has no contacts.</p>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
|
||||
<div v-if="contactEdit !== null" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="Name"
|
||||
v-model="contactNewName"
|
||||
/>
|
||||
<button
|
||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
||||
@click="onClickSaveName(contactEdit, contactNewName)"
|
||||
>
|
||||
<fa icon="save" />
|
||||
</button>
|
||||
<span class="inline-block w-2" />
|
||||
<button
|
||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
|
||||
@click="onClickCancelName()"
|
||||
>
|
||||
<fa icon="ban" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -213,32 +251,44 @@
|
||||
import { AxiosError } from "axios";
|
||||
import * as didJwt from "did-jwt";
|
||||
import * as R from "ramda";
|
||||
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import {
|
||||
accessToken,
|
||||
getContactPayloadFromJwtUrl,
|
||||
SimpleSigner,
|
||||
} from "@/libs/crypto";
|
||||
import {
|
||||
GiveServerRecord,
|
||||
GiveVerifiableCredential,
|
||||
RegisterVerifiableCredential,
|
||||
SERVICE_ID,
|
||||
} from "@/libs/endorserServer";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Buffer = require("buffer/").Buffer;
|
||||
|
||||
@Component({
|
||||
components: { AlertMessage, QuickNav, EntityIcon },
|
||||
components: { QuickNav, EntityIcon },
|
||||
})
|
||||
export default class ContactsView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
contacts: Array<Contact> = [];
|
||||
contactEndorserUrl = localStorage.getItem("contactEndorserUrl") || "";
|
||||
contactInput = "";
|
||||
contactEdit: Contact | null = null;
|
||||
contactNewName = "";
|
||||
// { "did:...": concatenated-descriptions } entry for each contact
|
||||
givenByMeDescriptions: Record<string, string> = {};
|
||||
// { "did:...": amount } entry for each contact
|
||||
@@ -256,12 +306,10 @@ export default class ContactsView extends Vue {
|
||||
showGiveNumbers = false;
|
||||
showGiveTotals = true;
|
||||
showGiveConfirmed = true;
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
|
||||
async created() {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
|
||||
@@ -274,12 +322,18 @@ export default class ContactsView extends Vue {
|
||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||
allContacts,
|
||||
);
|
||||
|
||||
if (this.contactEndorserUrl) {
|
||||
await this.newContactFromScan(this.contactEndorserUrl);
|
||||
localStorage.removeItem("contactEndorserUrl");
|
||||
this.contactEndorserUrl = "";
|
||||
}
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -290,7 +344,7 @@ export default class ContactsView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -299,7 +353,7 @@ export default class ContactsView extends Vue {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public async getHeadersAndIdentity(activeDid) {
|
||||
public async getHeadersAndIdentity(activeDid: string) {
|
||||
const identity = await this.getIdentity(activeDid);
|
||||
const headers = await this.getHeaders(identity);
|
||||
|
||||
@@ -308,11 +362,11 @@ export default class ContactsView extends Vue {
|
||||
|
||||
async loadGives() {
|
||||
const handleResponse = (
|
||||
resp,
|
||||
descriptions,
|
||||
confirmed,
|
||||
unconfirmed,
|
||||
useRecipient,
|
||||
resp: { status: number; data: { data: GiveServerRecord[] } },
|
||||
descriptions: Record<string, string>,
|
||||
confirmed: Record<string, number>,
|
||||
unconfirmed: Record<string, number>,
|
||||
useRecipient: boolean,
|
||||
) => {
|
||||
if (resp.status === 200) {
|
||||
const allData = resp.data.data;
|
||||
@@ -341,12 +395,11 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
title: "Server Error",
|
||||
text:
|
||||
"Got an error retrieving your " +
|
||||
resp.config.url.includes("recipientDid")
|
||||
? "received"
|
||||
: "given" + " time from the server.",
|
||||
(useRecipient ? "given" : "received") +
|
||||
" time from the server.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -401,7 +454,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
title: "Server Error",
|
||||
text: error as string,
|
||||
},
|
||||
-1,
|
||||
@@ -410,6 +463,18 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
|
||||
async onClickNewContact(): Promise<void> {
|
||||
if (!this.contactInput) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "No Contact",
|
||||
text: "There was no contact info to add.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let did = this.contactInput;
|
||||
let name, publicKeyBase64;
|
||||
const commaPos1 = this.contactInput.indexOf(",");
|
||||
@@ -428,12 +493,74 @@ export default class ContactsView extends Vue {
|
||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
||||
}
|
||||
const newContact = { did, name, publicKeyBase64 };
|
||||
await db.contacts.add(newContact);
|
||||
const allContacts = this.contacts.concat([newContact]);
|
||||
this.contacts = R.sort(
|
||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||
allContacts,
|
||||
);
|
||||
return this.addContact(newContact);
|
||||
}
|
||||
|
||||
async newContactFromScan(url: string): Promise<void> {
|
||||
const payload = getContactPayloadFromJwtUrl(url);
|
||||
if (!payload) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "No Contact Info",
|
||||
text: "The contact info could not be parsed.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
return this.addContact({
|
||||
did: payload.iss,
|
||||
name: payload.own.name,
|
||||
publicKeyBase64: payload.own.publicEncKey,
|
||||
} as Contact);
|
||||
}
|
||||
}
|
||||
|
||||
async addContact(newContact: Contact) {
|
||||
if (!newContact.did) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Incomplete Contact",
|
||||
text: "Cannot add a contact without a DID.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
return db.contacts
|
||||
.add(newContact)
|
||||
.then(() => {
|
||||
const allContacts = this.contacts.concat([newContact]);
|
||||
this.contacts = R.sort(
|
||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||
allContacts,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Contact added",
|
||||
text: newContact.name + " was added.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error when adding contact to storage:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Contact Not Added",
|
||||
text: "An error prevented importing.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteContact(contact: Contact) {
|
||||
@@ -457,9 +584,22 @@ export default class ContactsView extends Vue {
|
||||
confirm(
|
||||
"Are you sure you want to use one of your registrations for " +
|
||||
this.nameForDid(this.contacts, contact.did) +
|
||||
(contact.registered
|
||||
? " -- especially since they are already marked as registered"
|
||||
: "") +
|
||||
"?",
|
||||
)
|
||||
) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
text: "",
|
||||
title: "Registration submitted...",
|
||||
},
|
||||
1000,
|
||||
);
|
||||
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
|
||||
const vcClaim: RegisterVerifiableCredential = {
|
||||
@@ -542,7 +682,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
title: "Server Error",
|
||||
text: userMessage,
|
||||
},
|
||||
-1,
|
||||
@@ -567,36 +707,30 @@ export default class ContactsView extends Vue {
|
||||
contact.seesMe = visibility;
|
||||
db.contacts.update(contact.did, { seesMe: visibility });
|
||||
} else {
|
||||
console.error("Bad response setting visibility: ", resp.data);
|
||||
if (resp.data.error?.message) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: resp.data.error?.message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: "Bad server response of " + resp.status,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
"Got some bad server response when setting visibility: ",
|
||||
resp,
|
||||
);
|
||||
const message =
|
||||
resp.data.error?.message || "Bad server response of " + resp.status;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Got some server error when setting visibility:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: err as string,
|
||||
title: "Server Error",
|
||||
text: "Check connectivity and try again.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -608,6 +742,8 @@ export default class ContactsView extends Vue {
|
||||
this.apiServer +
|
||||
"/api/report/canDidExplicitlySeeMe?did=" +
|
||||
encodeURIComponent(contact.did);
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const headers = await this.getHeaders(identity);
|
||||
|
||||
try {
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
@@ -619,7 +755,7 @@ export default class ContactsView extends Vue {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
type: "info",
|
||||
title: "Refreshed",
|
||||
text:
|
||||
this.nameForContact(contact, true) +
|
||||
@@ -627,38 +763,29 @@ export default class ContactsView extends Vue {
|
||||
(visibility ? "" : "not ") +
|
||||
"see your activity.",
|
||||
},
|
||||
5000,
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
if (resp.data.error?.message) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: resp.data.error?.message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: "Bad server response of " + resp.status,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
console.log("Got bad server response when checking visibility: ", resp);
|
||||
const message = resp.data.error?.message || "Got bad server response.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Caught error from server request to check visibility:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: err as string,
|
||||
title: "Server Error",
|
||||
text: "Check connectivity and try again.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -685,11 +812,17 @@ export default class ContactsView extends Vue {
|
||||
|
||||
// if they have unconfirmed amounts, ask to confirm those first
|
||||
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
|
||||
const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are";
|
||||
const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours";
|
||||
if (
|
||||
confirm(
|
||||
"There are " +
|
||||
"There " +
|
||||
isare +
|
||||
" " +
|
||||
this.givenToMeUnconfirmed[fromDid] +
|
||||
" unconfirmed hours from them." +
|
||||
" unconfirmed " +
|
||||
hours +
|
||||
" from them." +
|
||||
" Would you like to confirm some of those hours?",
|
||||
)
|
||||
) {
|
||||
@@ -697,6 +830,7 @@ export default class ContactsView extends Vue {
|
||||
name: "contact-amounts",
|
||||
query: { contactDid: fromDid },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!this.isNumeric(this.hourInput)) {
|
||||
@@ -747,7 +881,9 @@ export default class ContactsView extends Vue {
|
||||
confirm(
|
||||
"Are you sure you want to record " +
|
||||
this.hourInput +
|
||||
" hours " +
|
||||
" hour" +
|
||||
(this.hourInput == "1" ? "" : "s") +
|
||||
" " +
|
||||
toFrom +
|
||||
description +
|
||||
"?",
|
||||
@@ -849,7 +985,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
title: "Server Error",
|
||||
text: userMessage,
|
||||
},
|
||||
-1,
|
||||
@@ -858,6 +994,18 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
private async onClickCancelName() {
|
||||
this.contactEdit = null;
|
||||
this.contactNewName = "";
|
||||
}
|
||||
|
||||
private async onClickSaveName(contact: Contact, newName: string) {
|
||||
contact.name = newName;
|
||||
return db.contacts
|
||||
.update(contact.did, { name: newName })
|
||||
.then(() => (this.contactEdit = null));
|
||||
}
|
||||
|
||||
public toggleShowGiveTotals() {
|
||||
if (this.showGiveTotals) {
|
||||
this.showGiveTotals = false;
|
||||
@@ -882,6 +1030,26 @@ export default class ContactsView extends Vue {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
||||
/* Tooltip container */
|
||||
.tooltip {
|
||||
@@ -889,7 +1057,6 @@ export default class ContactsView extends Vue {
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
||||
}
|
||||
|
||||
/* Tooltip text */
|
||||
.tooltip .tooltiptext {
|
||||
visibility: hidden;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</h1>
|
||||
|
||||
<!-- Quick Search -->
|
||||
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()">
|
||||
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchTerms"
|
||||
@@ -17,7 +17,7 @@
|
||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
||||
/>
|
||||
<button
|
||||
@click="search()"
|
||||
@click="searchAll()"
|
||||
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||
>
|
||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||
@@ -32,6 +32,8 @@
|
||||
href="#"
|
||||
@click="
|
||||
projects = [];
|
||||
isLocalActive = true;
|
||||
isRemoteActive = false;
|
||||
searchLocal();
|
||||
"
|
||||
v-bind:class="computedLocalTabClassNames()"
|
||||
@@ -49,10 +51,12 @@
|
||||
v-bind:class="computedRemoteTabClassNames()"
|
||||
@click="
|
||||
projects = [];
|
||||
search();
|
||||
isRemoteActive = true;
|
||||
isLocalActive = false;
|
||||
searchAll();
|
||||
"
|
||||
>
|
||||
Remote
|
||||
Anywhere
|
||||
<span
|
||||
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||
>{{ remoteCount }}</span
|
||||
@@ -62,6 +66,49 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="isLocalActive">
|
||||
<div v-if="!isChoosingSearchBox">
|
||||
<button
|
||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="isChoosingSearchBox = true"
|
||||
>
|
||||
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
||||
Choose Location Below for Nearby Search
|
||||
</button>
|
||||
<button
|
||||
v-if="isNewMarkerSet"
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="storeSearchBox"
|
||||
>
|
||||
Store This Location for Nearby Search
|
||||
</button>
|
||||
<button
|
||||
v-if="searchBox"
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="forgetSearchBox"
|
||||
>
|
||||
Delete Stored Location
|
||||
</button>
|
||||
<button
|
||||
v-if="isNewMarkerSet"
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="resetLatLong"
|
||||
>
|
||||
Reset Marker
|
||||
</button>
|
||||
<button
|
||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="cancelSearchBoxSelect"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</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"
|
||||
@@ -71,7 +118,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
<InfiniteScroll @reached-bottom="loadMoreData">
|
||||
<InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox">
|
||||
<ul>
|
||||
<li
|
||||
class="border-b border-slate-300"
|
||||
@@ -103,42 +150,103 @@
|
||||
</li>
|
||||
</ul>
|
||||
</InfiniteScroll>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
|
||||
<div
|
||||
v-if="isLocalActive && isChoosingSearchBox"
|
||||
style="height: 600px; width: 800px"
|
||||
>
|
||||
<l-map
|
||||
ref="map"
|
||||
:center="[localCenterLat, localCenterLong]"
|
||||
v-model:zoom="localZoom"
|
||||
@click="setMapPoint"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
layer-type="base"
|
||||
name="OpenStreetMap"
|
||||
/>
|
||||
<l-marker
|
||||
v-if="isNewMarkerSet"
|
||||
:lat-lng="[localCenterLat, localCenterLong]"
|
||||
@click="isNewMarkerSet = false"
|
||||
/>
|
||||
<l-rectangle
|
||||
v-if="isNewMarkerSet"
|
||||
:bounds="[
|
||||
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
||||
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
||||
]"
|
||||
:weight="1"
|
||||
/>
|
||||
</l-map>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { LeafletMouseEvent } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import {
|
||||
LMap,
|
||||
LMarker,
|
||||
LRectangle,
|
||||
LTileLayer,
|
||||
} from "@vue-leaflet/vue-leaflet";
|
||||
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { didInfo } from "@/libs/endorserServer";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import { didInfo, ProjectData } from "@/libs/endorserServer";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
|
||||
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||
const WORLD_ZOOM = 2;
|
||||
const DEFAULT_ZOOM = 2;
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon },
|
||||
components: {
|
||||
LRectangle,
|
||||
QuickNav,
|
||||
InfiniteScroll,
|
||||
EntityIcon,
|
||||
LMap,
|
||||
LMarker,
|
||||
LTileLayer,
|
||||
},
|
||||
})
|
||||
export default class DiscoverView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
searchTerms = "";
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
projects: ProjectData[] = [];
|
||||
isChoosingSearchBox = false;
|
||||
isLocalActive = true;
|
||||
isRemoteActive = false;
|
||||
isNewMarkerSet = false;
|
||||
localCenterLat = 0;
|
||||
localCenterLong = 0;
|
||||
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
localCount = 0;
|
||||
localZoom = DEFAULT_ZOOM;
|
||||
remoteCount = 0;
|
||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||
isLoading = false;
|
||||
|
||||
// make this function available to the Vue template
|
||||
@@ -149,6 +257,9 @@ export default class DiscoverView extends Vue {
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||
this.resetLatLong();
|
||||
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
|
||||
await accountsDB.open();
|
||||
@@ -158,8 +269,10 @@ export default class DiscoverView extends Vue {
|
||||
this.searchLocal();
|
||||
}
|
||||
|
||||
public async buildHeaders() {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
public async buildHeaders(): Promise<HeadersInit> {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (this.activeDid) {
|
||||
await accountsDB.open();
|
||||
@@ -180,16 +293,13 @@ export default class DiscoverView extends Vue {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public async search(beforeId?: string) {
|
||||
public async searchAll(beforeId?: string) {
|
||||
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(
|
||||
@@ -202,12 +312,13 @@ export default class DiscoverView extends Vue {
|
||||
|
||||
if (response.status !== 200) {
|
||||
const details = await response.text();
|
||||
console.log("Problem with full search:", details);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: `There was a problem accessing the server. Please try again later. (${details})`,
|
||||
text: `There was a problem accessing the server. Try again later.`,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -220,14 +331,15 @@ export default class DiscoverView extends Vue {
|
||||
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 });
|
||||
const { name, description, handleId, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, rowid });
|
||||
}
|
||||
this.remoteCount = this.projects.length;
|
||||
} else {
|
||||
throw JSON.stringify(results);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
console.log("Error with feed load:", e);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -244,14 +356,19 @@ export default class DiscoverView extends Vue {
|
||||
}
|
||||
|
||||
public async searchLocal(beforeId?: string) {
|
||||
if (!this.searchBox) {
|
||||
this.projects = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const claimContents =
|
||||
"claimContents=" + encodeURIComponent(this.searchTerms);
|
||||
let queryParams = [
|
||||
claimContents,
|
||||
"minLocLat=40.901000",
|
||||
"maxLocLat=40.904000",
|
||||
"westLocLon=-111.914000",
|
||||
"eastLocLon=-111.909000",
|
||||
"minLocLat=" + this.searchBox.bbox.minLat,
|
||||
"maxLocLat=" + this.searchBox.bbox.maxLat,
|
||||
"westLocLon=" + this.searchBox.bbox.westLong,
|
||||
"eastLocLon=" + this.searchBox.bbox.eastLong,
|
||||
].join("&");
|
||||
|
||||
if (beforeId) {
|
||||
@@ -260,8 +377,6 @@ export default class DiscoverView extends Vue {
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.isLocalActive = true;
|
||||
this.isRemoteActive = false;
|
||||
const response = await fetch(
|
||||
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
|
||||
{
|
||||
@@ -272,12 +387,13 @@ export default class DiscoverView extends Vue {
|
||||
|
||||
if (response.status !== 200) {
|
||||
const details = await response.text();
|
||||
console.log("Problem with nearby search:", details);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: `There was a problem accessing the server. Please try again later. (${details})`,
|
||||
text: "There was a problem accessing the server. Try again later.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -302,7 +418,8 @@ export default class DiscoverView extends Vue {
|
||||
} else {
|
||||
throw JSON.stringify(results);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
console.log("Error with feed load:", e);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -328,7 +445,7 @@ export default class DiscoverView extends Vue {
|
||||
if (this.isLocalActive) {
|
||||
this.searchLocal(latestProject["rowid"]);
|
||||
} else if (this.isRemoteActive) {
|
||||
this.search(latestProject["rowid"]);
|
||||
this.searchAll(latestProject["rowid"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,6 +462,128 @@ export default class DiscoverView extends Vue {
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
setMapPoint(event: LeafletMouseEvent) {
|
||||
if (this.isNewMarkerSet) {
|
||||
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
||||
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
||||
} else {
|
||||
// marker is not set
|
||||
this.localCenterLat = event.latlng.lat;
|
||||
this.localCenterLong = event.latlng.lng;
|
||||
|
||||
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
// Guess at a size for the bounding box.
|
||||
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
||||
const bounds = event.target.boxZoom?._map?.getBounds();
|
||||
if (bounds) {
|
||||
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
||||
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
||||
}
|
||||
this.localLatDiff = latDiff;
|
||||
this.localLongDiff = longDiff;
|
||||
this.isNewMarkerSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
public resetLatLong() {
|
||||
if (this.searchBox?.bbox) {
|
||||
const bbox = this.searchBox.bbox;
|
||||
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
||||
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
||||
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
||||
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
||||
this.localZoom = WORLD_ZOOM;
|
||||
this.isNewMarkerSet = true;
|
||||
} else {
|
||||
this.isNewMarkerSet = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async storeSearchBox() {
|
||||
if (this.localCenterLong || this.localCenterLat) {
|
||||
try {
|
||||
const newSearchBox = {
|
||||
name: "Local",
|
||||
bbox: {
|
||||
eastLong: this.localCenterLong + this.localLongDiff,
|
||||
maxLat: this.localCenterLat + this.localLatDiff,
|
||||
minLat: this.localCenterLat - this.localLatDiff,
|
||||
westLong: this.localCenterLong - this.localLongDiff,
|
||||
},
|
||||
};
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
searchBoxes: [newSearchBox],
|
||||
});
|
||||
this.searchBox = newSearchBox;
|
||||
this.isChoosingSearchBox = false;
|
||||
this.searchLocal();
|
||||
} catch (err) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Updating Search Settings",
|
||||
text: "Try going to a different page and then coming back.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error(
|
||||
"Telling user to retry the location search setting because:",
|
||||
err,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "No Location Selected",
|
||||
text: "Select a location on the map.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async forgetSearchBox() {
|
||||
try {
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
searchBoxes: [],
|
||||
});
|
||||
this.searchBox = null;
|
||||
this.localCenterLat = 0;
|
||||
this.localCenterLong = 0;
|
||||
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||
this.localZoom = DEFAULT_ZOOM;
|
||||
this.isChoosingSearchBox = false;
|
||||
this.isNewMarkerSet = false;
|
||||
this.searchLocal();
|
||||
} catch (err) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Updating Search Settings",
|
||||
text: "Try going to a different page and then coming back.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
console.error(
|
||||
"Telling user to retry the location search setting because:",
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public cancelSearchBoxSelect() {
|
||||
this.isChoosingSearchBox = false;
|
||||
this.localZoom = WORLD_ZOOM;
|
||||
}
|
||||
|
||||
public computedLocalTabClassNames() {
|
||||
return {
|
||||
"inline-block": true,
|
||||
|
||||
@@ -41,14 +41,15 @@
|
||||
You need someone to register you -- usually the person who told you
|
||||
about this app, on the Contacts
|
||||
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
|
||||
and after you have contacts, you can select any contact on the home page
|
||||
and record your appreciation for... whatever. That is a claim recorded
|
||||
you can select any contact on the home page (or "anonymous") and record
|
||||
your appreciation for... whatever. The main goal is to record what
|
||||
people have given you, to grow gifting economies. Each claim is recorded
|
||||
on a custom ledger. The day after being registered, you'll be able to
|
||||
register others; later, you can create projects, too.
|
||||
able to register others; later, you can create projects, too.
|
||||
</p>
|
||||
<p>
|
||||
Note that there are limits to how many each person can register, so you
|
||||
may have to wait.
|
||||
Note that there are limits to how many others each person can register,
|
||||
so you may have to wait.
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
|
||||
@@ -130,7 +131,9 @@
|
||||
|
||||
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
||||
<p>
|
||||
Go
|
||||
Before doing this, note that it is an advanced feature that affects
|
||||
functionality (eg. the words "Alt ID" next to results, backup features)
|
||||
so beware if you think that may cause confusion. You can
|
||||
<router-link to="start" class="text-blue-500">
|
||||
create another identity here.
|
||||
</router-link>
|
||||
@@ -181,7 +184,7 @@
|
||||
<script lang="ts">
|
||||
import * as Package from "../../package.json";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class Help extends Vue {
|
||||
|
||||
@@ -6,168 +6,82 @@
|
||||
Time Safari
|
||||
</h1>
|
||||
|
||||
<!-- show the actions for recognizing a give -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
|
||||
|
||||
<button
|
||||
@click="
|
||||
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>
|
||||
|
||||
<button
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-permission',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notification Permission
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold">Quick Action</h2>
|
||||
<p class="mb-4">Record a gift from a contact:</p>
|
||||
|
||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||
<li @click="openDialog()">
|
||||
<EntityIcon
|
||||
:entityId="Anonymous"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Anonymous
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
v-for="contact in allContacts"
|
||||
:key="contact.did"
|
||||
@click="openDialog(contact)"
|
||||
<div v-if="!activeDid">
|
||||
To record others' giving,
|
||||
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
||||
create your identifier.</router-link
|
||||
>
|
||||
<EntityIcon
|
||||
:entityId="contact.did"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isRegistered">
|
||||
To record others' giving, someone must register your account, so show
|
||||
them
|
||||
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
|
||||
your identity info</router-link
|
||||
>
|
||||
and then
|
||||
<router-link :to="{ name: 'account' }" class="text-blue-500">
|
||||
check your limits.</router-link
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- activeDid && isRegistered -->
|
||||
<h2 class="text-xl font-bold">Record a Gift</h2>
|
||||
|
||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||
<li @click="openDialog()">
|
||||
<EntityIcon
|
||||
:entityId="null"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Anonymous/Unnamed
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
v-for="contact in allContacts"
|
||||
:key="contact.did"
|
||||
@click="openDialog(contact)"
|
||||
>
|
||||
{{ contact.name || contact.did }}
|
||||
</h3>
|
||||
</li>
|
||||
</ul>
|
||||
<EntityIcon
|
||||
:entityId="contact.did"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{{ contact.name || contact.did }}
|
||||
</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…
|
||||
</router-link>
|
||||
<!-- 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…
|
||||
</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"
|
||||
v-if="allContacts.length === 0"
|
||||
>
|
||||
(No contacts to show.)
|
||||
<!-- 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"
|
||||
v-if="allContacts.length === 0"
|
||||
>
|
||||
(No contacts to show.)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GiftedDialog
|
||||
ref="customDialog"
|
||||
@dialog-result="handleDialogResult"
|
||||
message="Received from"
|
||||
>
|
||||
</GiftedDialog>
|
||||
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
|
||||
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
||||
@@ -196,40 +110,49 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<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 { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
|
||||
import {
|
||||
didInfo,
|
||||
GiverInputInfo,
|
||||
GiveServerRecord,
|
||||
} from "@/libs/endorserServer";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class HomeView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
feedAllLoaded = false;
|
||||
feedData = [];
|
||||
feedPreviousOldestId = null;
|
||||
feedLastViewedId = null;
|
||||
feedPreviousOldestId?: string;
|
||||
feedLastViewedId?: string;
|
||||
isHiddenSpinner = true;
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
isRegistered = false;
|
||||
numAccounts = 0;
|
||||
|
||||
async beforeCreate() {
|
||||
@@ -237,12 +160,12 @@ export default class HomeView extends Vue {
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -253,7 +176,7 @@ export default class HomeView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -269,13 +192,15 @@ export default class HomeView extends Vue {
|
||||
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.updateAllFeed();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -291,12 +216,16 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
|
||||
public async buildHeaders() {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
const headers: HeadersInit = {
|
||||
"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 account = allAccounts.find(
|
||||
(acc) => acc.did === this.activeDid,
|
||||
) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -314,7 +243,7 @@ export default class HomeView extends Vue {
|
||||
|
||||
public async updateAllFeed() {
|
||||
this.isHiddenSpinner = false;
|
||||
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
|
||||
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
|
||||
.then(async (results) => {
|
||||
if (results.data.length > 0) {
|
||||
this.feedData = this.feedData.concat(results.data);
|
||||
@@ -349,7 +278,7 @@ export default class HomeView extends Vue {
|
||||
this.isHiddenSpinner = true;
|
||||
}
|
||||
|
||||
public async retrieveClaims(endorserApiServer, identifier, beforeId) {
|
||||
public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
|
||||
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
||||
const response = await fetch(
|
||||
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
||||
@@ -372,24 +301,36 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
giveDescription(giveRecord) {
|
||||
let claim = giveRecord.fullClaim;
|
||||
if (claim.claim) {
|
||||
claim = claim.claim;
|
||||
}
|
||||
giveDescription(giveRecord: GiveServerRecord) {
|
||||
// similar code is in endorser-mobile utility.ts
|
||||
// claim.claim happen for some claims wrapped in a Verifiable Credential
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
|
||||
// agent.did is for legacy data, before March 2023
|
||||
const giverDid = claim.agent?.identifier || claim.agent?.did;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did;
|
||||
const giverInfo = didInfo(
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
this.allMyDids,
|
||||
this.allContacts,
|
||||
);
|
||||
const gaveAmount = claim.object?.amountOfThisGood
|
||||
let gaveAmount = claim.object?.amountOfThisGood
|
||||
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||
: claim.description || "something unknown";
|
||||
: "";
|
||||
if (claim.description) {
|
||||
if (gaveAmount) {
|
||||
gaveAmount = gaveAmount + ", and also: ";
|
||||
}
|
||||
gaveAmount = gaveAmount + claim.description;
|
||||
}
|
||||
if (!gaveAmount) {
|
||||
gaveAmount = "something not described";
|
||||
}
|
||||
// recipient.did is for legacy data, before March 2023
|
||||
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
||||
const gaveRecipientId =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
claim.recipient?.identifier || (claim.recipient as any)?.did;
|
||||
const gaveRecipientInfo = gaveRecipientId
|
||||
? " to " +
|
||||
didInfo(
|
||||
@@ -399,133 +340,19 @@ export default class HomeView extends Vue {
|
||||
this.allContacts,
|
||||
)
|
||||
: "";
|
||||
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
|
||||
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
||||
}
|
||||
|
||||
displayAmount(code, amt) {
|
||||
displayAmount(code: string, amt: number) {
|
||||
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
|
||||
}
|
||||
|
||||
currencyShortWordForCode(unitCode, single) {
|
||||
currencyShortWordForCode(unitCode: string, single: boolean) {
|
||||
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||
}
|
||||
|
||||
openDialog(giver) {
|
||||
this.$refs.customDialog.open(giver);
|
||||
}
|
||||
|
||||
handleDialogResult(result) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
this.recordGive(result.giver?.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 (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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
openDialog(giver: GiverInputInfo) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<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 }}
|
||||
{{ givenName }}
|
||||
</h2>
|
||||
<div class="text-sm text-slate-500 truncate">
|
||||
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||
@@ -49,7 +49,9 @@
|
||||
</ul>
|
||||
|
||||
<!-- Actions -->
|
||||
<!-- id used by puppeteer test script -->
|
||||
<router-link
|
||||
id="start-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"
|
||||
>
|
||||
@@ -62,52 +64,56 @@
|
||||
>
|
||||
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 { db, accountsDB } from "@/db/index";
|
||||
import { AccountsSchema } from "@/db/tables/accounts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { AlertMessage, QuickNav } })
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class IdentitySwitcherView extends Vue {
|
||||
Constants = AppString;
|
||||
public accounts: AccountsSchema;
|
||||
public activeDid;
|
||||
public firstName;
|
||||
public lastName;
|
||||
public alertTitle;
|
||||
public alertMessage;
|
||||
public otherIdentities = [];
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
Constants = AppString;
|
||||
public accounts: typeof AccountsSchema;
|
||||
public activeDid = "";
|
||||
public apiServer = "";
|
||||
public apiServerInput = "";
|
||||
public givenName = "";
|
||||
public otherIdentities: Array<{ did: string }> = [];
|
||||
public showContactGives = false;
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
const identity = JSON.parse((account?.identity as string) || "null");
|
||||
return identity;
|
||||
}
|
||||
|
||||
async created() {
|
||||
try {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.apiServerInput = settings?.apiServer || "";
|
||||
this.firstName = settings?.firstName || "No";
|
||||
this.lastName = settings?.lastName || "Name";
|
||||
this.givenName =
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
|
||||
this.showContactGives = !!settings?.showContactGivesInline;
|
||||
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
@@ -126,40 +132,29 @@ export default class IdentitySwitcherView extends Vue {
|
||||
}
|
||||
}
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Loading Accounts",
|
||||
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) {
|
||||
async switchAccount(did?: string) {
|
||||
// 0 means none
|
||||
if (did === "0") {
|
||||
did = undefined;
|
||||
}
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: did,
|
||||
});
|
||||
this.activeDid = did;
|
||||
this.activeDid = did || "";
|
||||
this.otherIdentities = [];
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
<p class="text-center text-xl mb-4 font-light">
|
||||
Enter your seed phrase below to import your identity on this device.
|
||||
</p>
|
||||
<!-- id used by puppeteer test script -->
|
||||
<input
|
||||
id="seed-input"
|
||||
type="text"
|
||||
placeholder="Seed Phrase"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
@@ -67,7 +69,7 @@ import {
|
||||
deriveAddress,
|
||||
newIdentifier,
|
||||
} from "../libs/crypto";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
deriveAddress,
|
||||
newIdentifier,
|
||||
} from "../libs/crypto";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
|
||||
@Component({
|
||||
@@ -87,9 +87,9 @@ export default class ImportAccountView extends Vue {
|
||||
async mounted() {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const seedDids = {};
|
||||
const seedDids: Record<string, Array<string>> = {};
|
||||
accounts.forEach((account) => {
|
||||
const prevDids = seedDids[account.mnemonic] || [];
|
||||
const prevDids: Array<string> = seedDids[account.mnemonic] || [];
|
||||
seedDids[account.mnemonic] = prevDids.concat([account.did]);
|
||||
});
|
||||
this.didArrays = Object.values(seedDids);
|
||||
@@ -107,9 +107,9 @@ export default class ImportAccountView extends Vue {
|
||||
public async incrementDerivation() {
|
||||
await accountsDB.open();
|
||||
// find the maximum derivation path for the selected DIDs
|
||||
const selectedArray: Array<string> = this.didArrays.find(
|
||||
(dids) => dids[0] === this.selectedArrayFirstDid,
|
||||
);
|
||||
const selectedArray: Array<string> =
|
||||
this.didArrays.find((dids) => dids[0] === this.selectedArrayFirstDid) ||
|
||||
[];
|
||||
const allMatchingAccounts = await accountsDB.accounts
|
||||
.where("did")
|
||||
.anyOf(...selectedArray)
|
||||
|
||||
@@ -10,21 +10,15 @@
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
</button>
|
||||
[New/Edit] Identity
|
||||
Edit Identity
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="First Name"
|
||||
placeholder="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"
|
||||
v-model="givenName"
|
||||
/>
|
||||
|
||||
<div class="mt-8">
|
||||
@@ -49,37 +43,31 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { db } from "@/db";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
|
||||
@Component({
|
||||
components: {},
|
||||
})
|
||||
export default class NewEditAccountView extends Vue {
|
||||
firstName =
|
||||
localStorage.getItem("firstName") === null
|
||||
? "--"
|
||||
: localStorage.getItem("firstName");
|
||||
lastName =
|
||||
localStorage.getItem("lastName") === null
|
||||
? "--"
|
||||
: localStorage.getItem("lastName");
|
||||
givenName = "";
|
||||
|
||||
// 'created' hook runs when the Vue instance is first created
|
||||
async created() {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
this.firstName = settings?.firstName || "";
|
||||
this.lastName = settings?.lastName || "";
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.givenName =
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
|
||||
}
|
||||
|
||||
onClickSaveChanges() {
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName,
|
||||
firstName: this.givenName,
|
||||
lastName: "", // deprecated, pre v 0.1.3
|
||||
});
|
||||
localStorage.setItem("firstName", this.firstName as string);
|
||||
localStorage.setItem("lastName", this.lastName as string);
|
||||
localStorage.setItem("firstName", this.givenName as string);
|
||||
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
|
||||
this.$router.push({ name: "account" });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<QuickNav selected="Projects"></QuickNav>
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Breadcrumb -->
|
||||
@@ -97,10 +98,6 @@
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -111,20 +108,28 @@ import * as didJwt from "did-jwt";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
|
||||
import { accountsDB, db } from "@/db";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||
import { useAppStore } from "@/store/app";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import { PlanVerifiableCredential } from "@/libs/endorserServer";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { AlertMessage, LMap, LMarker, LTileLayer },
|
||||
components: { LMap, LMarker, LTileLayer, QuickNav },
|
||||
})
|
||||
export default class NewEditProjectView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
apiServer = "";
|
||||
description = "";
|
||||
errorMessage = "";
|
||||
@@ -140,7 +145,7 @@ export default class NewEditProjectView extends Vue {
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
@@ -156,7 +161,7 @@ export default class NewEditProjectView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -215,7 +220,7 @@ export default class NewEditProjectView extends Vue {
|
||||
|
||||
private async SaveProject(identity: IIdentifier) {
|
||||
// Make a claim
|
||||
const vcClaim: VerifiableCredential = {
|
||||
const vcClaim: PlanVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "PlanAction",
|
||||
name: this.projectName,
|
||||
@@ -270,19 +275,15 @@ export default class NewEditProjectView extends Vue {
|
||||
// version shows up here: https://api.endorser.ch/api-docs/
|
||||
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
||||
this.errorMessage = "";
|
||||
this.alertTitle = "";
|
||||
this.alertMessage = "";
|
||||
|
||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
||||
// version shows up here: https://api.endorser.ch/api-docs/
|
||||
useAppStore().setProjectId(
|
||||
resp.data.success.handleId || resp.data.success.fullIri,
|
||||
);
|
||||
setTimeout(
|
||||
function (that: Vue) {
|
||||
const route = {
|
||||
name: "project",
|
||||
};
|
||||
that.$router.push(route);
|
||||
function (that: NewEditProjectView) {
|
||||
that.$router.push({ name: "project" });
|
||||
},
|
||||
2000,
|
||||
this,
|
||||
@@ -290,11 +291,13 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
} catch (error) {
|
||||
let userMessage = "There was an error saving the project.";
|
||||
const serverError = error as AxiosError;
|
||||
const serverError = error as AxiosError<{
|
||||
error?: { message?: string };
|
||||
}>;
|
||||
if (serverError) {
|
||||
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
||||
console.log(serverError);
|
||||
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.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -40,13 +40,13 @@
|
||||
<script lang="ts">
|
||||
import "dexie-export-import";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class AccountViewView extends Vue {
|
||||
export default class NewIdentifierView extends Vue {
|
||||
loading = true;
|
||||
|
||||
async mounted() {
|
||||
|
||||
@@ -95,14 +95,14 @@
|
||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||
<li @click="openDialog()">
|
||||
<EntityIcon
|
||||
:entityId="Anonymous"
|
||||
:entityId="null"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Anonymous
|
||||
Anonymous/Unnamed
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
<!-- 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"
|
||||
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"
|
||||
>
|
||||
@@ -196,44 +196,47 @@
|
||||
|
||||
<GiftedDialog
|
||||
ref="customDialog"
|
||||
@dialog-result="handleDialogResult"
|
||||
message="Received from"
|
||||
:projectId="this.projectId"
|
||||
>
|
||||
</GiftedDialog>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosError } from "axios";
|
||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
||||
import * as moment from "moment";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
GiverInputInfo,
|
||||
GiveServerRecord,
|
||||
} from "@/libs/endorserServer";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class ProjectViewView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeDid = "";
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
allMyDids: Array<string> = [];
|
||||
allContacts: Array<Contact> = [];
|
||||
apiServer = "";
|
||||
@@ -252,7 +255,7 @@ export default class ProjectViewView extends Vue {
|
||||
|
||||
async created() {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
@@ -261,17 +264,17 @@ export default class ProjectViewView extends Vue {
|
||||
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 account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
this.LoadProject(identity);
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -282,7 +285,7 @@ export default class ProjectViewView extends Vue {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public async getHeaders(identity) {
|
||||
public async getHeaders(identity: IIdentifier) {
|
||||
const token = await accessToken(identity);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -300,7 +303,12 @@ export default class ProjectViewView extends Vue {
|
||||
}
|
||||
|
||||
// Isn't there a better way to make this available to the template?
|
||||
didInfo(did, activeDid, dids, contacts) {
|
||||
didInfo(
|
||||
did: string,
|
||||
activeDid: string,
|
||||
dids: Array<string>,
|
||||
contacts: Array<Contact>,
|
||||
) {
|
||||
return didInfo(did, activeDid, dids, contacts);
|
||||
}
|
||||
|
||||
@@ -317,7 +325,7 @@ export default class ProjectViewView extends Vue {
|
||||
this.apiServer +
|
||||
"/api/claim/byHandle/" +
|
||||
encodeURIComponent(this.projectId);
|
||||
const headers = {
|
||||
const headers: RawAxiosRequestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (identity) {
|
||||
@@ -451,10 +459,6 @@ export default class ProjectViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
openDialog(contact) {
|
||||
this.$refs.customDialog.open(contact);
|
||||
}
|
||||
|
||||
getOpenStreetMapUrl() {
|
||||
// Google URL is https://maps.google.com/?q=LAT,LONG
|
||||
return (
|
||||
@@ -469,102 +473,8 @@ export default class ProjectViewView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
handleDialogResult(result) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
this.recordGive(result.contact?.did, result.description, result.hours);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
// action was not "confirm" so do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param giverDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
async recordGive(giverDid, description, hours) {
|
||||
if (!this.activeDid) {
|
||||
this.$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,
|
||||
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,
|
||||
);
|
||||
}
|
||||
openDialog(contact: GiverInputInfo) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(contact);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -67,28 +67,33 @@
|
||||
</li>
|
||||
</ul>
|
||||
</InfiniteScroll>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import EntityIcon from "@/components/EntityIcon";
|
||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { ProjectData } from "@/libs/endorserServer";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { InfiniteScroll, AlertMessage, QuickNav, EntityIcon },
|
||||
components: { InfiniteScroll, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class ProjectsView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
apiServer = "";
|
||||
projects: ProjectData[] = [];
|
||||
current: IIdentifier;
|
||||
@@ -119,21 +124,30 @@ export default class ProjectsView extends Vue {
|
||||
if (resp.status === 200 || !resp.data.data) {
|
||||
const plans: ProjectData[] = resp.data.data;
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId = plan.fullIri, rowid } = plan;
|
||||
const { name, description, handleId, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, rowid });
|
||||
}
|
||||
} else {
|
||||
console.log("Bad server response & data:", resp.status, resp.data);
|
||||
throw Error("Failed to get projects from the server.");
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to get projects from the server. Try again later.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error loading projects:", error.message);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Got error loading projects:", error.message || error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Got an error loading projects: " + error.message,
|
||||
text: "Got an error loading projects.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -177,7 +191,7 @@ export default class ProjectsView extends Vue {
|
||||
await this.dataLoader(url, token);
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid) {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
.where("did")
|
||||
|
||||
@@ -50,28 +50,34 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>You do not have an active identity.</div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB, db } from "@/db";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import * as R from "ramda";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { AlertMessage, QuickNav } })
|
||||
interface Account {
|
||||
mnemonic: string;
|
||||
}
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class SeedBackupView extends Vue {
|
||||
activeAccount = null;
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
activeAccount: Account | null | undefined = null;
|
||||
numAccounts = 0;
|
||||
showSeed = false;
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
|
||||
// 'created' hook runs when the Vue instance is first created
|
||||
async created() {
|
||||
@@ -84,7 +90,7 @@ export default class SeedBackupView extends Vue {
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
this.numAccounts = accounts.length;
|
||||
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
console.error("Got an error loading an identity:", err);
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
Start Here
|
||||
</h1>
|
||||
|
||||
<div class="mt-8">
|
||||
<!-- id used by puppeteer test script -->
|
||||
<div id="start-question" class="mt-8">
|
||||
<p class="text-center text-xl mb-4 font-light">
|
||||
Do you have an identity to import?
|
||||
</p>
|
||||
@@ -37,7 +38,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { accountsDB } from "@/db";
|
||||
import { accountsDB } from "@/db/index";
|
||||
|
||||
@Component({
|
||||
components: {},
|
||||
|
||||
@@ -34,25 +34,35 @@
|
||||
</div>
|
||||
<button class="float-right" @click="captureGraphics()">Screenshot</button>
|
||||
<div id="scene-container" class="h-screen"></div>
|
||||
<AlertMessage
|
||||
:alertTitle="alertTitle"
|
||||
:alertMessage="alertMessage"
|
||||
></AlertMessage>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
|
||||
import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { World } from "@/components/World/World.js";
|
||||
import AlertMessage from "@/components/AlertMessage";
|
||||
import QuickNav from "@/components/QuickNav";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { AlertMessage, World, QuickNav } })
|
||||
interface RendererSVGType {
|
||||
domElement: Element;
|
||||
}
|
||||
|
||||
interface Dictionary<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({ components: { World, QuickNav } })
|
||||
export default class StatisticsView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
world: World;
|
||||
worldProperties: WorldProperties = {};
|
||||
alertTitle = "";
|
||||
alertMessage = "";
|
||||
worldProperties: Dictionary<number> = {};
|
||||
|
||||
mounted() {
|
||||
try {
|
||||
@@ -60,14 +70,14 @@ export default class StatisticsView extends Vue {
|
||||
const newWorld = new World(container, this);
|
||||
newWorld.start();
|
||||
this.world = newWorld;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} catch (err: unknown) {
|
||||
const error = err as Error;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Mounting Error",
|
||||
text: err.message,
|
||||
text: error.message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -85,12 +95,12 @@ export default class StatisticsView extends Vue {
|
||||
ExportToSVG(rendererSVG, "test.svg");
|
||||
}
|
||||
|
||||
public setWorldProperty(propertyName, propertyValue) {
|
||||
public setWorldProperty(propertyName: string, propertyValue: number) {
|
||||
this.worldProperties[propertyName] = propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
function ExportToSVG(rendererSVG, filename) {
|
||||
function ExportToSVG(rendererSVG: RendererSVGType, filename: string) {
|
||||
const XMLS = new XMLSerializer();
|
||||
const svgfile = XMLS.serializeToString(rendererSVG.domElement);
|
||||
const svgData = svgfile;
|
||||
|
||||
151
src/views/TestView.vue
Normal file
151
src/views/TestView.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<QuickNav selected="Profile"></QuickNav>
|
||||
<!-- CONTENT -->
|
||||
<section id="Content" class="p-6 pb-24">
|
||||
<!-- Heading -->
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||
Test
|
||||
</h1>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
|
||||
|
||||
<button
|
||||
@click="
|
||||
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>
|
||||
|
||||
<button
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-permission',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif ON
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-mute',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif MUTE
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="
|
||||
this.$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-off',
|
||||
},
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif OFF
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class Help extends Vue {}
|
||||
</script>
|
||||
@@ -1,41 +1,47 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "./src",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/components/*": ["components/*"],
|
||||
"@/views/*": ["views/*"],
|
||||
"@/db/*": ["db/*"],
|
||||
"@/libs/*": ["libs/*"],
|
||||
"@/constants/*": ["constants/*"],
|
||||
"@/store/*": ["store/*"],
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
29
web-push.md
29
web-push.md
@@ -358,3 +358,32 @@ unsubscribeFromPush().catch((err) => {
|
||||
```
|
||||
|
||||
NOTE: We could offer an option within the app to "mute" these notifications. This wouldn't turn off the notifications at the browser level, but you could make it so that your Service Worker doesn't display them even if it receives them.
|
||||
|
||||
|
||||
## NOTIFICATION DIALOG WORKFLOW
|
||||
|
||||
# ON APP FIRST-LAUNCH:
|
||||
The user is periodically presented with the notification permission dialog that asks them if they want to turn on notifications. User is given 3 choices:
|
||||
|
||||
- "Turn on Notifications": triggers the browser's own notification permission prompt.
|
||||
- "Maybe Later": dismisses the dialog, to reappear at a later instance. (The next time the user launches the app? After X amount of days? A combination of both?)
|
||||
- "Never": dismisses the dialog; app remembers to not automatically present the dialog again.
|
||||
|
||||
# IF THE USER CHOOSES "NEVER":
|
||||
The dialog can still be accessed via the Notifications toggle switch in `AccountViewView` (which also tells the user if notifications are turned on or off).
|
||||
|
||||
# TO TEMPORARILY MUTE NOTIFICATIONS:
|
||||
While notifications are turned on, the user can tap on the Mute Notifications toggle switch in `AccountViewView` (visible only when notifications are turned on) to trigger the Mute Notifications Dialog. User is given the following choices:
|
||||
|
||||
- Several "Mute for X Hour/s" buttons to temporarily mute notifications.
|
||||
- "Mute until I turn it back on" button to indefinitely mute notifications.
|
||||
- "Cancel" to make no changes and dismiss the dialog.
|
||||
|
||||
# TO UNMUTE NOTIFICATIONS:
|
||||
Simply tap on the Mute Notifications toggle switch in `AccountViewView` to immediately unmute notifications. No dialog needed.
|
||||
|
||||
# TO TURN OFF NOTIFICATIONS:
|
||||
While notifications are turned on, the user can tap on the App Notifications toggle switch in `AccountViewView` to trigger the Turn Off Notifications Dialog. User is given the following choices:
|
||||
|
||||
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
|
||||
- "Leave it On" to make no changes and dismiss the dialog.
|
||||
Reference in New Issue
Block a user