forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
110 Commits
quicknav-c
...
update-api
| Author | SHA1 | Date | |
|---|---|---|---|
| ac4c92d8e8 | |||
| 937a3cb6c6 | |||
|
|
304985f88d | ||
|
|
9a41aff8f0 | ||
|
|
e19cd980b4 | ||
| 194f741984 | |||
| b31c0d975c | |||
| f09684d7cd | |||
| 1767a48a7f | |||
| d2e2fc707e | |||
| bf6830a1a8 | |||
|
|
fe09f5180d | ||
|
|
4de66b1609 | ||
|
|
4b87692231 | ||
|
|
503bb1bd93 | ||
|
|
9fa3b8be0b | ||
| 3b1a9b9c5b | |||
|
|
f55e50067f | ||
| 7f48149d6f | |||
| c5b4921583 | |||
| b28689ad06 | |||
| 0444b5be32 | |||
| 4866416aae | |||
| e48a4ed05b | |||
| 87cfead094 | |||
| 179a5cd9f8 | |||
|
|
eff67c2a4a | ||
|
|
db22d559b7 | ||
|
|
c4443f2ed1 | ||
|
|
05a7758c65 | ||
| d0ec7930e1 | |||
| 3e2cd1291c | |||
| 8d42fe905d | |||
| a7e98c8f1a | |||
| f07c804b24 | |||
|
|
e8eae544f3 | ||
|
|
7c77578f79 | ||
|
|
34636d6047 | ||
|
|
5134e2f562 | ||
| 91b46eaaee | |||
| 31d1a449ae | |||
| 1248132076 | |||
| 015704c94e | |||
| 540ef916c2 | |||
| bee7c87a8f | |||
| 6bbc88f86c | |||
| 624abbb179 | |||
| 110ed009b2 | |||
| a5892238d5 | |||
| 8eb80a9ede | |||
|
|
32125133f0 | ||
|
|
47ade49e31 | ||
|
|
47ce91cca1 | ||
|
|
3e52b504b0 | ||
|
|
4ecea1ab0e | ||
|
|
b9fdc920ea | ||
|
|
0907d59a6a | ||
| 59ce15c744 | |||
|
|
9960a96a20 | ||
|
|
098c6c0fa0 | ||
|
|
ead37ede74 | ||
|
|
f428199228 | ||
|
|
1405b88323 | ||
|
|
44fc2850dd | ||
|
|
52d411470e | ||
|
|
ab678a900a | ||
|
|
efa59e170f | ||
|
|
7a4ceaa455 | ||
| d7a9fb6d54 | |||
| 78b98bab5e | |||
| 2493f2ad39 | |||
| cf2b80b1f5 | |||
| 00954693b5 | |||
| 2dd77f898f | |||
| c1f218c2f3 | |||
| b5e78e5dc8 | |||
| b86323ec83 | |||
| 8add6448fb | |||
| 47442655cb | |||
|
|
1d362c314b | ||
| 3eda246e85 | |||
|
|
3f13d3ea33 | ||
|
|
cef346e487 | ||
|
|
fed23a61ee | ||
|
|
b6b7c56157 | ||
| 3f8be3b4de | |||
| 21af37c2c2 | |||
| 0b7a35c9b8 | |||
| 0257901c5b | |||
| d9d6096275 | |||
| ed7d37c649 | |||
| 81dd6eb595 | |||
|
|
c61bb88788 | ||
|
|
3bd55f3ad2 | ||
|
|
3471afdf25 | ||
|
|
e25a83ff1b | ||
|
|
0fbdb45d3e | ||
|
|
dc23ba1375 | ||
|
|
08137eb000 | ||
|
|
5d49965166 | ||
| 8e8aa4356d | |||
| 59a354027e | |||
|
|
5dc80ce12a | ||
|
|
754bced2a9 | ||
|
|
e3f58bd593 | ||
|
|
3b41014083 | ||
| f568149745 | |||
| a27d035e9b | |||
| 16d0be681c | |||
| e42b3ff11d |
990
package-lock.json
generated
990
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@@ -21,45 +21,46 @@
|
|||||||
"@veramo/did-provider-ethr": "^5.1.2",
|
"@veramo/did-provider-ethr": "^5.1.2",
|
||||||
"@veramo/did-resolver": "^5.2.0",
|
"@veramo/did-resolver": "^5.2.0",
|
||||||
"@veramo/key-manager": "^5.1.2",
|
"@veramo/key-manager": "^5.1.2",
|
||||||
"@vueuse/core": "^10.2.0",
|
"@vueuse/core": "^10.2.1",
|
||||||
"@zxing/text-encoding": "^0.9.0",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"core-js": "^3.31.0",
|
"core-js": "^3.31.1",
|
||||||
"dexie": "^3.2.4",
|
"dexie": "^3.2.4",
|
||||||
"dexie-export-import": "^4.0.7",
|
"dexie-export-import": "^4.0.7",
|
||||||
"did-jwt": "^7.2.2",
|
"did-jwt": "^7.2.4",
|
||||||
"ethereum-cryptography": "^2.0.0",
|
"ethereum-cryptography": "^2.0.0",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"ethr-did-resolver": "^8.0.0",
|
"ethr-did-resolver": "^8.0.0",
|
||||||
|
"jdenticon": "^3.2.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"localstorage-slim": "^2.4.0",
|
"localstorage-slim": "^2.4.0",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
"merkletreejs": "^0.3.10",
|
"merkletreejs": "^0.3.10",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
"pinia-plugin-persistedstate": "^3.1.0",
|
"pinia-plugin-persistedstate": "^3.1.0",
|
||||||
"qr-code-generator-vue3": "^1.4.21",
|
"qr-code-generator-vue3": "^1.4.21",
|
||||||
"ramda": "^0.29.0",
|
"ramda": "^0.29.0",
|
||||||
"readable-stream": "^4.4.0",
|
"readable-stream": "^4.4.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"three": "^0.153.0",
|
"three": "^0.154.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-class-component": "^8.0.0-0",
|
|
||||||
"vue-facing-decorator": "^2.1.20",
|
"vue-facing-decorator": "^2.1.20",
|
||||||
"vue-property-decorator": "^9.1.2",
|
"vue-property-decorator": "^9.1.2",
|
||||||
"vue-router": "^4.2.2",
|
"vue-router": "^4.2.3",
|
||||||
"web-did-resolver": "^2.0.24"
|
"web-did-resolver": "^2.0.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ramda": "^0.29.2",
|
"@types/ramda": "^0.29.3",
|
||||||
"@types/three": "^0.152.1",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.60.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-pwa": "~5.0.8",
|
"@vue/cli-plugin-pwa": "~5.0.8",
|
||||||
@@ -69,13 +70,13 @@
|
|||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.43.0",
|
"eslint": "^8.44.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.15.0",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "~5.1.3"
|
"typescript": "~5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,62 @@
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- replace user-affecting console.log & console.error with error messages (eg. catches)
|
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
|
||||||
- if there's no identity, handle it on pages which expect an identity (eg. project -- look for JSON.parse identity calls)
|
|
||||||
- .1 show an appropriate message when there are no contacts
|
|
||||||
|
|
||||||
- 8 Move to vue-facing-decorator
|
|
||||||
|
|
||||||
- 01 design ideas for simple gives on the first page
|
|
||||||
|
|
||||||
- .1 remove commitments from ProjectView UI
|
|
||||||
- 01 add list of 'give' records for a project on ProjectView UI
|
|
||||||
|
|
||||||
- 02 Discover page - display results (currently in console.log), spin when searching
|
|
||||||
- 08 search by location, endpoint, etc assignee:trent
|
|
||||||
- 01 add a location for a project via map pin :
|
- 01 add a location for a project via map pin :
|
||||||
- give attribute to use assignee:trent
|
- add with a "location" field containing this: { "geo":{ "@type":"GeoCoordinates", "latitude":40.883944, "longitude":-111.884787 } }
|
||||||
- 01 remove all the "form" fields (or at least investigate to see if that page refresh is desired)
|
- 04 search by a bounding box for local projects (see API by clicking on "Nearby")
|
||||||
|
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:jose
|
||||||
|
- 02 Fix images on projectview - allow choice of image from a pallete of images or a url image.
|
||||||
|
|
||||||
- 08 Scan QR code to import into contacts.
|
- 08 Scan QR code to import into contacts.
|
||||||
|
|
||||||
- contacts v1 :
|
- 40 notifications :
|
||||||
- 01 Import contact info a la QR code.
|
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
|
||||||
- .2 move all "identity" references to temporary account access assignee:trent
|
|
||||||
|
|
||||||
- contacts v+ :
|
|
||||||
- .2 show error to user when adding a duplicate contact
|
|
||||||
- 01 parse input more robustly (with CSV lib and not commas)
|
|
||||||
|
|
||||||
- refactor UI :
|
- refactor UI :
|
||||||
- .5 Alerts show at the top and can be missed, eg. account data download
|
- .5 Alerts show at the top and can be missed if you've scrolled down on the page, eg. account data download
|
||||||
- 01 Change alert popup code a component (to cut down duplicate code; see "in many files")
|
- .2 Make alerts at the top more visible (because they're currently a similar color and sometimes aren't seen)
|
||||||
- 01 Change "nav" tabs across the bottom into a component (eliminating duplicate code).
|
|
||||||
- .5 Fix how icons show on top of bottom bar on ContactAmounts page
|
|
||||||
- .2 Hide "Advanced" section in Account page by default
|
|
||||||
|
|
||||||
- show pop-up confirming that settings & contacts have been downloaded
|
- Show pop-up or some message confirming that settings & contacts download has been initiated/finished
|
||||||
|
|
||||||
- Ensure each action sent to the server has a confirmation - registration
|
- Ensure each action sent to the server has a confirmation - eg registration
|
||||||
|
|
||||||
- Home Feed & Quick Give screen :
|
- Home Feed & Quick Give screen :
|
||||||
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
||||||
- 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
|
- 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
|
||||||
|
|
||||||
- .5 customize favicon
|
|
||||||
- .2 Hide "Advanced" section in Account page by default
|
|
||||||
- 04 allow user to download claims, mine + ones I can see about me from others
|
|
||||||
|
|
||||||
- 24 Move to Vite
|
- 24 Move to Vite
|
||||||
|
|
||||||
- 40 notifications :
|
- .5 add link to further project / people when a project pays ahead
|
||||||
- push
|
- .5 add project ID to the URL, to make a project publicly-accessible
|
||||||
|
- .5 remove edit from project page for projects owned by others
|
||||||
|
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
|
||||||
|
- .2 there are three dots at the top of ProjectViewView that refreshes the page but doesn't do anything else
|
||||||
|
- 01 fix images on project page, on discovery page
|
||||||
|
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist
|
||||||
|
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?)
|
||||||
|
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
||||||
|
- .2 move 'switch identity' to the advanced section
|
||||||
|
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
||||||
|
|
||||||
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
||||||
|
|
||||||
- 01 fix images on project page, on discovery page
|
- contacts v+ :
|
||||||
- .2 fix "Rotary" and static icon to the right on project page
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
|
- .2 show error to user when adding a duplicate contact
|
||||||
|
- 01 parse input more robustly (with CSV lib and not commas)
|
||||||
|
|
||||||
- stats v1 :
|
- stats v1 :
|
||||||
- 01 show numeric stats
|
- 01 show numeric stats
|
||||||
- 01 link to world for specific stats
|
- 01 link to world for specific stats
|
||||||
- .5 don't load another instance of a bush if it already exists
|
- .5 don't load another instance of a bush if it already exists
|
||||||
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
||||||
- 4-8 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
||||||
|
|
||||||
- Do we want split first name & last name?
|
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages
|
||||||
- remove 'about' page
|
- .5 customize favicon
|
||||||
|
- 04 allow user to download claims, mine + ones I can see about me from others
|
||||||
|
- Do we want to combine first name & last name?
|
||||||
|
- Show a warning if both giver and recipient are the same (but still allow?)
|
||||||
|
|
||||||
- Release Minimum Viable Product :
|
- Release Minimum Viable Product :
|
||||||
- 08 thorough testing for errors & edge cases
|
- 08 thorough testing for errors & edge cases
|
||||||
@@ -78,13 +69,11 @@ tasks:
|
|||||||
- Test PWA features on Android and iOS.
|
- Test PWA features on Android and iOS.
|
||||||
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
||||||
|
|
||||||
- 40 notifications v+ :
|
|
||||||
- pull, w/ scheduled runs
|
|
||||||
|
|
||||||
- linking between projects or plans :
|
- linking between projects or plans :
|
||||||
- terminology:
|
- show total time given to & from a project
|
||||||
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
- terminology:
|
||||||
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
|
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
||||||
|
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
|
||||||
|
|
||||||
- Stats :
|
- Stats :
|
||||||
- 01 point out user's location on the world
|
- 01 point out user's location on the world
|
||||||
@@ -104,11 +93,14 @@ tasks:
|
|||||||
|
|
||||||
- Peer DID
|
- Peer DID
|
||||||
|
|
||||||
|
|
||||||
- DIDComm
|
- DIDComm
|
||||||
|
|
||||||
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
|
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
|
||||||
|
|
||||||
|
- Do we want split first name & last name?
|
||||||
|
|
||||||
|
- 40 notifications v+ :
|
||||||
|
- pull, w/ scheduled runs
|
||||||
|
|
||||||
log:
|
log:
|
||||||
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
||||||
|
|||||||
127
src/App.vue
127
src/App.vue
@@ -1,5 +1,132 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|
||||||
|
<NotificationGroup group="alert">
|
||||||
|
<div
|
||||||
|
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
|
||||||
|
>
|
||||||
|
<Notification
|
||||||
|
v-slot="{ notifications, close }"
|
||||||
|
enter="transform ease-out duration-300 transition"
|
||||||
|
enter-from="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4"
|
||||||
|
enter-to="translate-y-0 opacity-100 sm:translate-x-0"
|
||||||
|
leave="transition ease-in duration-500"
|
||||||
|
leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0"
|
||||||
|
move="transition duration-500"
|
||||||
|
move-delay="delay-300"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="notification in notifications"
|
||||||
|
:key="notification.id"
|
||||||
|
class="w-full"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'toast'"
|
||||||
|
class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div class="w-full px-4 py-3">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'info'"
|
||||||
|
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-100 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
|
||||||
|
>
|
||||||
|
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
|
||||||
|
>
|
||||||
|
<fa icon="xmark" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'success'"
|
||||||
|
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-emerald-100 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
|
||||||
|
>
|
||||||
|
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
|
||||||
|
>
|
||||||
|
<fa icon="xmark" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'warning'"
|
||||||
|
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-amber-100 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
|
||||||
|
>
|
||||||
|
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
|
||||||
|
>
|
||||||
|
<fa icon="xmark" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'danger'"
|
||||||
|
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-rose-100 rounded-lg shadow-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
|
||||||
|
>
|
||||||
|
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
||||||
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
|
<p class="text-sm">{{ notification.text }}</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
|
||||||
|
>
|
||||||
|
<fa icon="xmark" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Notification>
|
||||||
|
</div>
|
||||||
|
</NotificationGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-bind:class="computedAlertClassNames()">
|
<div v-bind:class="computedAlertClassNames()">
|
||||||
<button
|
<button
|
||||||
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
|
class="close-button bg-amber-400 w-8 leading-loose rounded-full absolute top-2 right-2"
|
||||||
@click="onClickClose()"
|
@click="onClickClose()"
|
||||||
>
|
>
|
||||||
<fa icon="xmark"></fa>
|
<fa icon="xmark"></fa>
|
||||||
@@ -28,7 +28,7 @@ export default class AlertMessage extends Vue {
|
|||||||
return {
|
return {
|
||||||
hidden: !this.isAlertVisible,
|
hidden: !this.isAlertVisible,
|
||||||
"dismissable-alert": true,
|
"dismissable-alert": true,
|
||||||
"bg-slate-100": true,
|
"bg-amber-200": true,
|
||||||
"p-5": true,
|
"p-5": true,
|
||||||
rounded: true,
|
rounded: true,
|
||||||
"drop-shadow-lg": true,
|
"drop-shadow-lg": true,
|
||||||
|
|||||||
19
src/components/EntityIcon.vue
Normal file
19
src/components/EntityIcon.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div v-html="generateIdenticon()" class="w-fit"></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
import { toSvg } from "jdenticon";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class EntityIcon extends Vue {
|
||||||
|
@Prop entityId = "";
|
||||||
|
@Prop iconSize = "";
|
||||||
|
|
||||||
|
generateIdenticon() {
|
||||||
|
const svgString = toSvg(this.entityId, this.iconSize);
|
||||||
|
return svgString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,41 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="visible" class="dialog-overlay">
|
<div v-if="visible" class="dialog-overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<h1 class="text-lg text-center">
|
<h1 class="text-xl font-bold text-center mb-4">
|
||||||
{{ message }} {{ giver?.name || "somebody not specified" }}
|
{{ message }} {{ giver?.name || "somebody not specified" }}
|
||||||
</h1>
|
</h1>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
placeholder="What was received"
|
placeholder="What was received"
|
||||||
v-model="description"
|
v-model="description"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row mb-6">
|
||||||
<span class="py-4">Hours</span>
|
<span
|
||||||
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2"
|
||||||
|
>Hours</span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
|
@click="decrement()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" />
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="block w-8 rounded border border-slate-400 ml-4 text-center"
|
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
||||||
v-model="hours"
|
v-model="hours"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col px-1">
|
<div
|
||||||
<div>
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
<fa icon="square-caret-up" size="2xl" @click="increment()" />
|
@click="increment()"
|
||||||
</div>
|
>
|
||||||
<div>
|
<fa icon="chevron-right" />
|
||||||
<fa icon="square-caret-down" size="2xl" @click="decrement()" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-right">Sign & Send to publish to the world</p>
|
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
|
||||||
<div class="text-right">
|
<button
|
||||||
<button class="rounded border border-slate-400" @click="confirm">
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
<span class="m-2">Sign & Send</span>
|
@click="confirm"
|
||||||
</button>
|
>
|
||||||
|
Sign & Send
|
||||||
<button class="rounded border border-slate-400" @click="cancel">
|
</button>
|
||||||
<span class="m-2">Cancel</span>
|
<button
|
||||||
</button>
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
</div>
|
@click="cancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -106,12 +116,14 @@ export default class GiftedDialog extends Vue {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
width: 50%;
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br />
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>vue-cli documentation</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>babel</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>pwa</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>router</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>vuex</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>eslint</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>typescript</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>Forum</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>Community Chat</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
|
|
||||||
>Twitter</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>vue-router</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/vue-devtools#vue-devtools"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>vue-devtools</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
|
|
||||||
>vue-loader</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/vuejs/awesome-vue"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>awesome-vue</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from "vue-class-component";
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
props: {
|
|
||||||
msg: String,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class HelloWorld extends Vue {
|
|
||||||
msg!: string;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -24,7 +24,7 @@ export default class InfiniteScroll extends Vue {
|
|||||||
};
|
};
|
||||||
this.observer = new IntersectionObserver(
|
this.observer = new IntersectionObserver(
|
||||||
this.handleIntersection,
|
this.handleIntersection,
|
||||||
options
|
options,
|
||||||
);
|
);
|
||||||
this.observer.observe(this.$refs.sentinel as HTMLElement);
|
this.observer.observe(this.$refs.sentinel as HTMLElement);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ function createCamera() {
|
|||||||
35, // fov = Field Of View
|
35, // fov = Field Of View
|
||||||
1, // aspect ratio (dummy value)
|
1, // aspect ratio (dummy value)
|
||||||
0.1, // near clipping plane
|
0.1, // near clipping plane
|
||||||
350 // far clipping plane
|
350, // far clipping plane
|
||||||
);
|
);
|
||||||
|
|
||||||
// move the camera back so we can view the scene
|
// move the camera back so we can view the scene
|
||||||
|
|||||||
@@ -22,17 +22,16 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
const token = await accessToken(identity);
|
|
||||||
|
|
||||||
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
};
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
if (identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
headers["Authorization"] = "Bearer " + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
|
||||||
const resp = await axios.get(url, { headers: headers });
|
const resp = await axios.get(url, { headers: headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const landmarks = resp.data.data;
|
const landmarks = resp.data.data;
|
||||||
@@ -63,7 +62,11 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
|
|
||||||
// calculate positions for each claim, especially because some are random
|
// calculate positions for each claim, especially because some are random
|
||||||
const locations = landmarks.map((claim) =>
|
const locations = landmarks.map((claim) =>
|
||||||
locForGive(claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS)
|
locForGive(
|
||||||
|
claim,
|
||||||
|
world.PLATFORM_SIZE,
|
||||||
|
world.PLATFORM_EDGE_FOR_UNKNOWNS,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
@@ -93,7 +96,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
undefined,
|
undefined,
|
||||||
function (error) {
|
function (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// calculate when lights shine on appearing claim area
|
// calculate when lights shine on appearing claim area
|
||||||
@@ -121,7 +124,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
.onComplete(() => {
|
.onComplete(() => {
|
||||||
scene.remove(light);
|
scene.remove(light);
|
||||||
light.dispose();
|
light.dispose();
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.start();
|
.start();
|
||||||
world.lights = [...world.lights, light];
|
world.lights = [...world.lights, light];
|
||||||
@@ -130,18 +133,18 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
console.error(
|
console.error(
|
||||||
"Got bad server response status & data of",
|
"Got bad server response status & data of",
|
||||||
resp.status,
|
resp.status,
|
||||||
resp.data
|
resp.data,
|
||||||
);
|
);
|
||||||
vue.setAlert(
|
vue.setAlert(
|
||||||
"Error With Server",
|
"Error With Server",
|
||||||
"There was an error retrieving your claims from the server."
|
"There was an error retrieving your claims from the server.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Got exception contacting server:", error);
|
console.error("Got exception contacting server:", error);
|
||||||
vue.setAlert(
|
vue.setAlert(
|
||||||
"Error With Server",
|
"Error With Server",
|
||||||
"There was a problem retrieving your claims from the server."
|
"There was a problem retrieving your claims from the server.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
export enum AppString {
|
export enum AppString {
|
||||||
APP_NAME = "Kick-Start with Time",
|
APP_NAME = "Kick-Start with Time",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://endorser.ch:3000",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
|
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
|
||||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ if (localStorage.getItem("secret") == null) {
|
|||||||
localStorage.setItem("secret", secret);
|
localStorage.setItem("secret", secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log("IndexedDB Encryption Secret:", secret);
|
|
||||||
encrypted(accountsDB, { secretKey: secret });
|
encrypted(accountsDB, { secretKey: secret });
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const newIdentifier = (
|
|||||||
address: string,
|
address: string,
|
||||||
publicHex: string,
|
publicHex: string,
|
||||||
privateHex: string,
|
privateHex: string,
|
||||||
derivationPath: string
|
derivationPath: string,
|
||||||
): Omit<IIdentifier, keyof "provider"> => {
|
): Omit<IIdentifier, keyof "provider"> => {
|
||||||
return {
|
return {
|
||||||
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
||||||
@@ -46,7 +46,7 @@ export const newIdentifier = (
|
|||||||
* @return {*} {[string, string, string, string]}
|
* @return {*} {[string, string, string, string]}
|
||||||
*/
|
*/
|
||||||
export const deriveAddress = (
|
export const deriveAddress = (
|
||||||
mnemonic: string
|
mnemonic: string,
|
||||||
): [string, string, string, string] => {
|
): [string, string, string, string] => {
|
||||||
const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
|
const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
|
||||||
mnemonic = mnemonic.trim().toLowerCase();
|
mnemonic = mnemonic.trim().toLowerCase();
|
||||||
@@ -134,7 +134,7 @@ export function fromJose(signature: string): {
|
|||||||
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
|
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
|
||||||
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`
|
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const r = bytesToHex(signatureBytes.slice(0, 32));
|
const r = bytesToHex(signatureBytes.slice(0, 32));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { Axios, AxiosResponse } from "axios";
|
import { Axios, AxiosResponse } from "axios";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||||
export const SERVICE_ID = "endorser.ch";
|
export const SERVICE_ID = "endorser.ch";
|
||||||
@@ -81,12 +82,17 @@ export function isHiddenDid(did) {
|
|||||||
/**
|
/**
|
||||||
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
||||||
**/
|
**/
|
||||||
export function didInfo(did, identifiers, contacts) {
|
export function didInfo(
|
||||||
const myId = R.find((i) => i.did === did, identifiers);
|
did: string,
|
||||||
|
activeDid: string,
|
||||||
|
allMyDids: Array<string>,
|
||||||
|
contacts: Array<Contact>,
|
||||||
|
): string {
|
||||||
|
const myId: string | undefined = R.find(R.equals(did), allMyDids, did);
|
||||||
if (myId) {
|
if (myId) {
|
||||||
return "You";
|
return "You" + (myId !== activeDid ? " (Alt ID)" : "");
|
||||||
} else {
|
} else {
|
||||||
const contact = R.find((c) => c.did === did, contacts);
|
const contact: Contact | undefined = R.find((c) => c.did === did, contacts);
|
||||||
if (contact) {
|
if (contact) {
|
||||||
return contact.name || "Someone Unnamed in Contacts";
|
return contact.name || "Someone Unnamed in Contacts";
|
||||||
} else if (!did) {
|
} else if (!did) {
|
||||||
@@ -100,7 +106,7 @@ export function didInfo(did, identifiers, contacts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
* @param identity
|
* @param identity
|
||||||
* @param fromDid may be null
|
* @param fromDid may be null
|
||||||
@@ -116,7 +122,7 @@ export async function createAndSubmitGive(
|
|||||||
toDid: string,
|
toDid: string,
|
||||||
description: string,
|
description: string,
|
||||||
hours: number,
|
hours: number,
|
||||||
fulfillsProjectHandleId?: string
|
fulfillsProjectHandleId?: string,
|
||||||
): Promise<AxiosResponse<ClaimResult> | InternalError> {
|
): Promise<AxiosResponse<ClaimResult> | InternalError> {
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim: GiveVerifiableCredential = {
|
const vcClaim: GiveVerifiableCredential = {
|
||||||
@@ -193,3 +199,53 @@ export function isNumeric(str: string): boolean {
|
|||||||
export function numberOrZero(str: string): number {
|
export function numberOrZero(str: string): number {
|
||||||
return isNumeric(str) ? +str : 0;
|
return isNumeric(str) ? +str : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error?: {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimits {
|
||||||
|
doneClaimsThisWeek: string;
|
||||||
|
doneRegistrationsThisMonth: string;
|
||||||
|
maxClaimsPerWeek: string;
|
||||||
|
maxRegistrationsPerMonth: string;
|
||||||
|
nextMonthBeginDateTime: string;
|
||||||
|
nextWeekBeginDateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents data about a project
|
||||||
|
**/
|
||||||
|
export interface ProjectData {
|
||||||
|
/**
|
||||||
|
* Name of the project
|
||||||
|
**/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Description of the project
|
||||||
|
**/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* URL referencing information about the project
|
||||||
|
**/
|
||||||
|
handleId: string;
|
||||||
|
/**
|
||||||
|
* The Identier of the project
|
||||||
|
**/
|
||||||
|
rowid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifiableCredential {
|
||||||
|
"@context": string;
|
||||||
|
"@type": string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
identifier?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorldProperties {
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function didProviderName(netName: string) {
|
|||||||
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
|
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
|
||||||
|
|
||||||
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
|
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
|
||||||
DEFAULT_DID_PROVIDER_NETWORK_NAME
|
DEFAULT_DID_PROVIDER_NETWORK_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const HANDY_APP = false;
|
export const HANDY_APP = false;
|
||||||
|
|||||||
12
src/main.ts
12
src/main.ts
@@ -5,6 +5,7 @@ import "./registerServiceWorker";
|
|||||||
import router from "./router";
|
import router from "./router";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import VueAxios from "vue-axios";
|
import VueAxios from "vue-axios";
|
||||||
|
import Notifications from "notiwind";
|
||||||
|
|
||||||
import "./assets/styles/tailwind.css";
|
import "./assets/styles/tailwind.css";
|
||||||
|
|
||||||
@@ -13,12 +14,15 @@ import {
|
|||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
faChevronRight,
|
||||||
faCircle,
|
faCircle,
|
||||||
faCircleCheck,
|
faCircleCheck,
|
||||||
|
faCircleInfo,
|
||||||
faCircleQuestion,
|
faCircleQuestion,
|
||||||
faCircleUser,
|
faCircleUser,
|
||||||
faClock,
|
faClock,
|
||||||
faCoins,
|
faCoins,
|
||||||
|
faComment,
|
||||||
faCopy,
|
faCopy,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -43,6 +47,7 @@ import {
|
|||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
faXmark,
|
faXmark,
|
||||||
@@ -52,12 +57,15 @@ library.add(
|
|||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
|
faChevronRight,
|
||||||
faCircle,
|
faCircle,
|
||||||
faCircleCheck,
|
faCircleCheck,
|
||||||
|
faCircleInfo,
|
||||||
faCircleQuestion,
|
faCircleQuestion,
|
||||||
faCircleUser,
|
faCircleUser,
|
||||||
faClock,
|
faClock,
|
||||||
faCoins,
|
faCoins,
|
||||||
|
faComment,
|
||||||
faCopy,
|
faCopy,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -82,9 +90,10 @@ library.add(
|
|||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
faXmark
|
faXmark,
|
||||||
);
|
);
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
@@ -94,4 +103,5 @@ createApp(App)
|
|||||||
.use(createPinia())
|
.use(createPinia())
|
||||||
.use(VueAxios, axios)
|
.use(VueAxios, axios)
|
||||||
.use(router)
|
.use(router)
|
||||||
|
.use(Notifications)
|
||||||
.mount("#app");
|
.mount("#app");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
ready() {
|
ready() {
|
||||||
console.log(
|
console.log(
|
||||||
"App is being served from cache by a service worker.\n" +
|
"App is being served from cache by a service worker.\n" +
|
||||||
"For more details, visit https://goo.gl/AFskqB"
|
"For more details, visit https://goo.gl/AFskqB",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
registered() {
|
registered() {
|
||||||
@@ -24,7 +24,7 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
},
|
},
|
||||||
offline() {
|
offline() {
|
||||||
console.log(
|
console.log(
|
||||||
"No internet connection found. App is running in offline mode."
|
"No internet connection found. App is running in offline mode.",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
error(error) {
|
error(error) {
|
||||||
|
|||||||
@@ -25,12 +25,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
|
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
|
||||||
beforeEnter: enterOrStart,
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/about",
|
|
||||||
name: "about",
|
|
||||||
component: () =>
|
|
||||||
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/account",
|
path: "/account",
|
||||||
name: "account",
|
name: "account",
|
||||||
@@ -129,6 +123,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
|
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/identity-switcher",
|
||||||
|
name: "identity-switcher",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/project",
|
path: "/project",
|
||||||
name: "project",
|
name: "project",
|
||||||
@@ -164,6 +166,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
|
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/contact-gives",
|
||||||
|
name: "contact-gives",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "statistics" */ "../views/ContactGiftingView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @type {*} */
|
/** @type {*} */
|
||||||
@@ -172,4 +182,14 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const errorHandler = (error, to, from) => {
|
||||||
|
// Handle the error here
|
||||||
|
console.error(error, to, from);
|
||||||
|
console.log("XXXXX");
|
||||||
|
|
||||||
|
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
||||||
|
};
|
||||||
|
|
||||||
|
router.onError(errorHandler); // Assign the error handler to the router instance
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Options, Vue } from "vue-class-component";
|
|
||||||
|
|
||||||
@Options({
|
|
||||||
components: {},
|
|
||||||
})
|
|
||||||
export default class AboutView extends Vue {}
|
|
||||||
</script>
|
|
||||||
@@ -3,10 +3,23 @@
|
|||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
||||||
Your Identity
|
Your Identity
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span />
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'contact-qr' }"
|
||||||
|
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="qrcode" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-2">
|
<div class="flex justify-between py-2">
|
||||||
<span />
|
<span />
|
||||||
<span>
|
<span>
|
||||||
@@ -53,14 +66,6 @@
|
|||||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
<span v-show="showDidCopy">Copied!</span>
|
<span v-show="showDidCopy">Copied!</span>
|
||||||
<span class="whitespace-nowrap ml-4">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contact-qr' }"
|
|
||||||
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md ml-1"
|
|
||||||
>
|
|
||||||
<fa icon="qrcode" class="fa-fw"></fa>
|
|
||||||
</router-link>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
||||||
@@ -108,10 +113,16 @@
|
|||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'new-edit-account' }"
|
:to="{ name: 'new-edit-account' }"
|
||||||
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
|
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
Edit Identity
|
Edit Identity
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'identity-switcher' }"
|
||||||
|
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
|
||||||
|
>
|
||||||
|
Switch Identity / No Identity
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||||
|
|
||||||
@@ -132,133 +143,131 @@
|
|||||||
|
|
||||||
<!-- QR code popup -->
|
<!-- QR code popup -->
|
||||||
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
|
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
|
||||||
<form method="dialog">
|
<div class="text-slate-500 text-center">
|
||||||
<div class="text-slate-500 text-center">
|
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
||||||
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
</div>
|
||||||
</div>
|
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
|
||||||
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
value="cancel"
|
value="cancel"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
Copy to Clipboard
|
Copy to Clipboard
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
value="cancel"
|
value="cancel"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Advanced</h3>
|
<h3
|
||||||
|
class="text-sm uppercase font-semibold mb-3"
|
||||||
<label
|
@click="showAdvanced = !showAdvanced"
|
||||||
for="toggleShowAmounts"
|
|
||||||
class="flex items-center cursor-pointer mb-6"
|
|
||||||
@click="handleChange"
|
|
||||||
>
|
>
|
||||||
<!-- toggle -->
|
Advanced
|
||||||
<div class="relative">
|
</h3>
|
||||||
<!-- input -->
|
<div v-if="showAdvanced">
|
||||||
<input
|
<label
|
||||||
type="checkbox"
|
for="toggleShowAmounts"
|
||||||
v-model="showContactGives"
|
class="flex items-center cursor-pointer mb-6"
|
||||||
name="showContactGives"
|
@click="handleChange"
|
||||||
class="sr-only"
|
|
||||||
/>
|
|
||||||
<!-- line -->
|
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
|
||||||
<!-- dot -->
|
|
||||||
<div
|
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<!-- label -->
|
|
||||||
<div class="ml-2">Show amounts given with contacts</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex py-2">
|
|
||||||
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
|
||||||
Check Limits
|
|
||||||
</button>
|
|
||||||
<!-- show spinner if loading limits -->
|
|
||||||
<div v-if="loadingLimits" class="ml-2">
|
|
||||||
Checking... <fa icon="spinner" class="fa-spin"></fa>
|
|
||||||
</div>
|
|
||||||
<div class="ml-2">
|
|
||||||
{{ limitsMessage }}
|
|
||||||
</div>
|
|
||||||
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
|
||||||
<span class="font-bold">Rate Limits</span>
|
|
||||||
<p>
|
|
||||||
You have done {{ limits.doneClaimsThisWeek }} claims out of
|
|
||||||
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
|
|
||||||
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
You have done {{ limits.doneRegistrationsThisMonth }} registrations
|
|
||||||
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
|
|
||||||
registrations counter resets at
|
|
||||||
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex py-2">
|
|
||||||
Claim Server
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
|
||||||
v-model="apiServerInput"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-if="apiServerInput != apiServer"
|
|
||||||
class="px-4 rounded bg-red-500 border border-slate-400"
|
|
||||||
@click="onClickSaveApiServer()"
|
|
||||||
>
|
>
|
||||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
<!-- toggle -->
|
||||||
</button>
|
<div class="relative">
|
||||||
<button
|
<!-- input -->
|
||||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
<input
|
||||||
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)"
|
type="checkbox"
|
||||||
>
|
v-model="showContactGives"
|
||||||
Use Prod
|
name="showContactGives"
|
||||||
</button>
|
class="sr-only"
|
||||||
<button
|
/>
|
||||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
<!-- line -->
|
||||||
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)"
|
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||||
>
|
<!-- dot -->
|
||||||
Use Test
|
<div
|
||||||
</button>
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
<button
|
></div>
|
||||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
</div>
|
||||||
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)"
|
<!-- label -->
|
||||||
>
|
<div class="ml-2">Show amounts given with contacts</div>
|
||||||
Use Local
|
</label>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="numAccounts > 0" class="flex py-2">
|
<div class="flex py-2">
|
||||||
Switch Identifier
|
<button
|
||||||
<span v-for="accountNum in numAccounts" :key="accountNum">
|
class="text-center text-md text-blue-500"
|
||||||
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
|
@click="checkLimits()"
|
||||||
#{{ accountNum }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="text-blue-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'statistics' }"
|
|
||||||
class="block text-center py-3"
|
|
||||||
>
|
>
|
||||||
See Achievements & Statistics
|
Check Limits
|
||||||
</router-link>
|
</button>
|
||||||
</button>
|
<!-- show spinner if loading limits -->
|
||||||
|
<div v-if="loadingLimits" class="ml-2">
|
||||||
|
Checking... <fa icon="spinner" class="fa-spin"></fa>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2">
|
||||||
|
{{ limitsMessage }}
|
||||||
|
</div>
|
||||||
|
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
||||||
|
<span class="font-bold">Rate Limits</span>
|
||||||
|
<p>
|
||||||
|
You have done {{ limits.doneClaimsThisWeek }} claims out of
|
||||||
|
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
|
||||||
|
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You have done {{ limits.doneRegistrationsThisMonth }} registrations
|
||||||
|
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
|
||||||
|
registrations counter resets at
|
||||||
|
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex py-2">
|
||||||
|
Claim Server
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||||
|
v-model="apiServerInput"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="apiServerInput != apiServer"
|
||||||
|
class="px-4 rounded bg-red-500 border border-slate-400"
|
||||||
|
@click="onClickSaveApiServer()"
|
||||||
|
>
|
||||||
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||||
|
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)"
|
||||||
|
>
|
||||||
|
Use Prod
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||||
|
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)"
|
||||||
|
>
|
||||||
|
Use Test
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||||
|
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)"
|
||||||
|
>
|
||||||
|
Use Local
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="text-blue-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'statistics' }"
|
||||||
|
class="block text-center py-3"
|
||||||
|
>
|
||||||
|
See Achievements & Statistics
|
||||||
|
</router-link>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
:alertTitle="alertTitle"
|
:alertTitle="alertTitle"
|
||||||
@@ -269,7 +278,6 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
import * as R from "ramda";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
@@ -280,19 +288,11 @@ import { accessToken } from "@/libs/crypto";
|
|||||||
import { AxiosError } from "axios/index";
|
import { AxiosError } from "axios/index";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
|
||||||
interface RateLimits {
|
|
||||||
doneClaimsThisWeek: string;
|
|
||||||
doneRegistrationsThisMonth: string;
|
|
||||||
maxClaimsPerWeek: string;
|
|
||||||
maxRegistrationsPerMonth: string;
|
|
||||||
nextMonthBeginDateTime: string;
|
|
||||||
nextWeekBeginDateTime: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({ components: { AlertMessage, QuickNav } })
|
@Component({ components: { AlertMessage, QuickNav } })
|
||||||
export default class AccountViewView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
Constants = AppString;
|
Constants = AppString;
|
||||||
@@ -316,6 +316,29 @@ export default class AccountViewView extends Vue {
|
|||||||
showB64Copy = false;
|
showB64Copy = false;
|
||||||
showPubCopy = false;
|
showPubCopy = false;
|
||||||
|
|
||||||
|
showAdvanced = false;
|
||||||
|
alertMessage = "";
|
||||||
|
alertTitle = "";
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
||||||
doCopyTwoSecRedo(text, fn) {
|
doCopyTwoSecRedo(text, fn) {
|
||||||
fn();
|
fn();
|
||||||
@@ -333,7 +356,11 @@ export default class AccountViewView extends Vue {
|
|||||||
return timeStr.substring(0, timeStr.indexOf("T"));
|
return timeStr.substring(0, timeStr.indexOf("T"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
// Uncomment this to register this user on the test server.
|
// Uncomment this to register this user on the test server.
|
||||||
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
|
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
|
||||||
@@ -351,27 +378,42 @@ export default class AccountViewView extends Vue {
|
|||||||
this.lastName = settings?.lastName || "";
|
this.lastName = settings?.lastName || "";
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
this.numAccounts = accounts.length;
|
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
this.publicHex = identity.keys[0].publicKeyHex;
|
|
||||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
|
||||||
this.derivationPath = identity.keys[0].meta.derivationPath;
|
|
||||||
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
if (identity) {
|
||||||
activeDid: identity.did,
|
this.publicHex = identity.keys[0].publicKeyHex;
|
||||||
});
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString(
|
||||||
this.checkLimits();
|
"base64",
|
||||||
|
);
|
||||||
|
this.derivationPath = identity.keys[0].meta.derivationPath;
|
||||||
|
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: identity.did,
|
||||||
|
});
|
||||||
|
this.checkLimitsFor(identity);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertMessage =
|
if (
|
||||||
"Clear your cache and start over (after data backup).";
|
err.message ===
|
||||||
console.error("Telling user to clear cache at page create because:", err);
|
"Attempted to load account records with no identity available."
|
||||||
this.alertTitle = "Error Creating Account";
|
) {
|
||||||
|
this.limitsMessage = "No identity.";
|
||||||
|
this.loadingLimits = false;
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Creating Account",
|
||||||
|
text: "Clear your cache and start over (after data backup).",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to clear cache at page create because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,13 +424,19 @@ export default class AccountViewView extends Vue {
|
|||||||
showContactGivesInline: this.showContactGives,
|
showContactGivesInline: this.showContactGives,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertMessage =
|
this.$notify(
|
||||||
"Clear your cache and start over (after data backup).";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Contact Setting",
|
||||||
|
text: "Clear your cache and start over (after data backup).",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Telling user to clear cache after contact setting update because:",
|
"Telling user to clear cache after contact setting update because:",
|
||||||
err
|
err,
|
||||||
);
|
);
|
||||||
this.alertTitle = "Error Updating Contact Setting";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,66 +452,99 @@ export default class AccountViewView extends Vue {
|
|||||||
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
this.alertTitle = "Download Started";
|
this.$notify(
|
||||||
this.alertMessage = "See your downloads directory for the backup.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Download Started",
|
||||||
|
text: "See your downloads directory for the backup.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.alertTitle = "Export Error";
|
this.$notify(
|
||||||
this.alertMessage = "See console logs for more info.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Export Error",
|
||||||
|
text: "See console logs for more info.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
console.error("Export Error:", error);
|
console.error("Export Error:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkLimits() {
|
async checkLimits() {
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
if (identity) {
|
||||||
|
this.checkLimitsFor(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkLimitsFor(identity: IIdentifier) {
|
||||||
this.loadingLimits = true;
|
this.loadingLimits = true;
|
||||||
this.limitsMessage = "";
|
this.limitsMessage = "";
|
||||||
|
|
||||||
const url = this.apiServer + "/api/report/rateLimits";
|
|
||||||
await accountsDB.open();
|
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
const token = await accessToken(identity);
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const url = this.apiServer + "/api/report/rateLimits";
|
||||||
|
const headers = await this.getHeaders(identity);
|
||||||
|
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
// axios throws an exception on a 400
|
// axios throws an exception on a 400
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.limits = resp.data;
|
this.limits = resp.data;
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const serverError = error as AxiosError;
|
if (
|
||||||
|
error.message ===
|
||||||
|
"Attempted to load Give records with no identity available."
|
||||||
|
) {
|
||||||
|
this.limitsMessage = "No identity.";
|
||||||
|
this.loadingLimits = false;
|
||||||
|
} else {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
console.error("Bad response retrieving limits: ", serverError);
|
||||||
|
|
||||||
console.error("Bad response retrieving limits: ", serverError);
|
const data: ErrorResponse | undefined =
|
||||||
// Anybody know how to access items inside "response.data" without this?
|
serverError.response && serverError.response.data;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
if (data && data.error && data.error.message) {
|
||||||
const data: any = serverError.response?.data;
|
this.limitsMessage = data.error.message;
|
||||||
this.limitsMessage = data?.error?.message || "Bad server response.";
|
} else {
|
||||||
|
this.limitsMessage = "Bad server response.";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingLimits = false;
|
this.loadingLimits = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchAccount(accountNum: number) {
|
async switchAccount(accountNum: number) {
|
||||||
await accountsDB.open();
|
// 0 means none
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
if (accountNum === 0) {
|
||||||
const account = accounts[accountNum - 1];
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: undefined,
|
||||||
|
});
|
||||||
|
this.activeDid = "";
|
||||||
|
this.derivationPath = "";
|
||||||
|
this.publicHex = "";
|
||||||
|
this.publicBase64 = "";
|
||||||
|
} else {
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = accounts[accountNum - 1];
|
||||||
|
|
||||||
await db.open();
|
await db.open();
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
activeDid: account.did,
|
activeDid: account.did,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.activeDid = account.did;
|
this.activeDid = account.did;
|
||||||
this.derivationPath = account.derivationPath;
|
this.derivationPath = account.derivationPath;
|
||||||
this.publicHex = account.publicKeyHex;
|
this.publicHex = account.publicKeyHex;
|
||||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public showContactGivesClassNames() {
|
public showContactGivesClassNames() {
|
||||||
@@ -484,9 +565,5 @@ export default class AccountViewView extends Vue {
|
|||||||
setApiServerInput(value) {
|
setApiServerInput(value) {
|
||||||
this.apiServerInput = value;
|
this.apiServerInput = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
alertMessage = "";
|
|
||||||
alertTitle = "";
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,42 +15,40 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form>
|
<p class="text-center text-xl mb-4 font-light">
|
||||||
<p class="text-center text-xl mb-4 font-light">
|
Would you like to add <i>Firstname</i> to your network?
|
||||||
Would you like to add <i>Firstname</i> to your network?
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Account Details -->
|
<!-- Account Details -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<h2 class="text-xl font-semibold mb-2">Firstname Lastname</h2>
|
<h2 class="text-xl font-semibold mb-2">Firstname Lastname</h2>
|
||||||
|
|
||||||
<div class="text-slate-500 text-sm font-bold">ID</div>
|
<div class="text-slate-500 text-sm font-bold">ID</div>
|
||||||
<div class="text-sm text-slate-500 mb-1">
|
<div class="text-sm text-slate-500 mb-1">
|
||||||
<span><code>did:peer:kl45kj41lk451kl3</code></span>
|
<span><code>did:peer:kl45kj41lk451kl3</code></span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
value="Add Contact"
|
value="Add Contact"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ConfirmContactView extends Vue {}
|
export default class ConfirmContactView extends Vue {}
|
||||||
|
|||||||
@@ -1,52 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
|
||||||
<ul class="flex text-2xl p-2 gap-2">
|
|
||||||
<!-- Home Feed -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
|
|
||||||
><fa icon="house-chimney" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Search -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'discover' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="magnifying-glass" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Contacts -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'projects' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="folder-open" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Contacts -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contacts' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="users" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
<!-- Profile -->
|
|
||||||
<li class="basis-1/5 rounded-md text-slate-500">
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="block text-center py-3 px-1"
|
|
||||||
><fa icon="circle-user" class="fa-fw"></fa
|
|
||||||
></router-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Given with {{ contact?.name }}
|
Given with {{ contact?.name }}
|
||||||
</h1>
|
</h1>
|
||||||
|
<div class="flex justify-around">
|
||||||
|
<span />
|
||||||
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<div>
|
<div>
|
||||||
@@ -133,8 +95,9 @@ import {
|
|||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
|
||||||
@Component({ components: { AlertMessage } })
|
@Component({ components: { AlertMessage, QuickNav } })
|
||||||
export default class ContactsView extends Vue {
|
export default class ContactsView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
@@ -142,8 +105,38 @@ export default class ContactsView extends Vue {
|
|||||||
giveRecords: Array<GiveServerRecord> = [];
|
giveRecords: Array<GiveServerRecord> = [];
|
||||||
alertTitle = "";
|
alertTitle = "";
|
||||||
alertMessage = "";
|
alertMessage = "";
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -158,38 +151,31 @@ export default class ContactsView extends Vue {
|
|||||||
this.loadGives(this.activeDid, this.contact);
|
this.loadGives(this.activeDid, this.contact);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertTitle = "Error";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
err.userMessage ||
|
group: "alert",
|
||||||
"There was an error retrieving the latest sweet, sweet action.";
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
err.userMessage ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadGives(activeDid: string, contact: Contact) {
|
async loadGives(activeDid: string, contact: Contact) {
|
||||||
// only load the private keys temporarily when needed
|
|
||||||
await accountsDB.open();
|
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// load all the time I have given to them
|
|
||||||
try {
|
try {
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/gives?agentDid=" +
|
"/api/v2/report/gives?agentDid=" +
|
||||||
encodeURIComponent(identity.did) +
|
encodeURIComponent(identity.did) +
|
||||||
"&recipientDid=" +
|
"&recipientDid=" +
|
||||||
encodeURIComponent(contact.did);
|
encodeURIComponent(contact.did);
|
||||||
const token = await accessToken(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
result = resp.data.data;
|
result = resp.data.data;
|
||||||
@@ -197,11 +183,17 @@ export default class ContactsView extends Vue {
|
|||||||
console.error(
|
console.error(
|
||||||
"Got bad response status & data of",
|
"Got bad response status & data of",
|
||||||
resp.status,
|
resp.status,
|
||||||
resp.data
|
resp.data,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Got an error retrieving your given time from the server.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
);
|
);
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
this.alertMessage =
|
|
||||||
"Got an error retrieving your given time from the server.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const url2 =
|
const url2 =
|
||||||
@@ -210,11 +202,7 @@ export default class ContactsView extends Vue {
|
|||||||
encodeURIComponent(contact.did) +
|
encodeURIComponent(contact.did) +
|
||||||
"&recipientDid=" +
|
"&recipientDid=" +
|
||||||
encodeURIComponent(identity.did);
|
encodeURIComponent(identity.did);
|
||||||
const token2 = await accessToken(identity);
|
const headers2 = await this.getHeaders(identity);
|
||||||
const headers2 = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token2,
|
|
||||||
};
|
|
||||||
const resp2 = await this.axios.get(url2, { headers: headers2 });
|
const resp2 = await this.axios.get(url2, { headers: headers2 });
|
||||||
if (resp2.status === 200) {
|
if (resp2.status === 200) {
|
||||||
result = R.concat(result, resp2.data.data);
|
result = R.concat(result, resp2.data.data);
|
||||||
@@ -222,22 +210,35 @@ export default class ContactsView extends Vue {
|
|||||||
console.error(
|
console.error(
|
||||||
"Got bad response status & data of",
|
"Got bad response status & data of",
|
||||||
resp2.status,
|
resp2.status,
|
||||||
resp2.data
|
resp2.data,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Got an error retrieving your given time from the server.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
);
|
);
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
this.alertMessage =
|
|
||||||
"Got an error retrieving your given time from the server.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedResult: Array<GiveServerRecord> = R.sort(
|
const sortedResult: Array<GiveServerRecord> = R.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
|
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
|
||||||
result
|
result,
|
||||||
);
|
);
|
||||||
this.giveRecords = sortedResult;
|
this.giveRecords = sortedResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = error as string;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: error as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,13 +267,7 @@ export default class ContactsView extends Vue {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create a signature using private key of identity
|
// Create a signature using private key of identity
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
if (identity.keys[0].privateKeyHex !== null) {
|
if (identity.keys[0].privateKeyHex !== null) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
||||||
@@ -296,7 +291,6 @@ export default class ContactsView extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
//console.log("Got resp data:", resp.data);
|
|
||||||
if (resp.data?.success) {
|
if (resp.data?.success) {
|
||||||
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
|
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
|
||||||
}
|
}
|
||||||
@@ -313,15 +307,29 @@ export default class ContactsView extends Vue {
|
|||||||
userMessage = error as string;
|
userMessage = error as string;
|
||||||
}
|
}
|
||||||
// Now set that error for the user to see.
|
// Now set that error for the user to see.
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = userMessage;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cannotConfirmMessage() {
|
cannotConfirmMessage() {
|
||||||
this.alertTitle = "Not Allowed";
|
this.$notify(
|
||||||
this.alertMessage = "Only the recipient can confirm final receipt.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Not Allowed",
|
||||||
|
text: "Only the recipient can confirm final receipt.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
311
src/views/ContactGiftingView.vue
Normal file
311
src/views/ContactGiftingView.vue
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Home"></QuickNav>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
<!-- Back -->
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'home' }"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
|
></router-link>
|
||||||
|
|
||||||
|
Give to Contacts
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Search -->
|
||||||
|
|
||||||
|
<!-- Initial Loading Animation -->
|
||||||
|
|
||||||
|
<!-- Results List -->
|
||||||
|
<ul class="border-t border-slate-300">
|
||||||
|
<li class="border-b border-slate-300 py-3">
|
||||||
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
|
<span class="grow italic text-slate-500"
|
||||||
|
><EntityIcon
|
||||||
|
entityId="Anonymous"
|
||||||
|
:iconSize="32"
|
||||||
|
class="opacity-50 inline-block align-middle border border-dashed border-slate-400 bg-slate-200 rounded-md mr-1"
|
||||||
|
></EntityIcon>
|
||||||
|
Anonymous
|
||||||
|
</span>
|
||||||
|
<span class="text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openDialog()"
|
||||||
|
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="gift" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-for="contact in allContacts"
|
||||||
|
:key="contact.did"
|
||||||
|
class="border-b border-slate-300 py-3"
|
||||||
|
>
|
||||||
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
|
<span class="grow font-semibold"
|
||||||
|
><EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="32"
|
||||||
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||||
|
></EntityIcon>
|
||||||
|
{{ contact.name || "(no name)" }}
|
||||||
|
</span>
|
||||||
|
<span class="text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openDialog(contact)"
|
||||||
|
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
<fa icon="gift" class="fa-fw"></fa>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<GiftedDialog
|
||||||
|
ref="customDialog"
|
||||||
|
@dialog-result="handleDialogResult"
|
||||||
|
message="Received from"
|
||||||
|
>
|
||||||
|
</GiftedDialog>
|
||||||
|
<AlertMessage
|
||||||
|
:alertTitle="alertTitle"
|
||||||
|
:alertMessage="alertMessage"
|
||||||
|
></AlertMessage>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import { db, accountsDB } from "@/db";
|
||||||
|
import { AccountsSchema } from "@/db/tables/accounts";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import { createAndSubmitGive } from "@/libs/endorserServer";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||||
|
})
|
||||||
|
export default class HomeView extends Vue {
|
||||||
|
activeDid = "";
|
||||||
|
allAccounts: Array<Account> = [];
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
apiServer = "";
|
||||||
|
isHiddenSpinner = true;
|
||||||
|
alertTitle = "";
|
||||||
|
alertMessage = "";
|
||||||
|
accounts: AccountsSchema;
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
accountsDB.open();
|
||||||
|
this.accounts = accountsDB.accounts;
|
||||||
|
this.numAccounts = await this.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||||
|
this.updateAllFeed();
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
err.userMessage ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async buildHeaders() {
|
||||||
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
|
if (this.activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = allAccounts.find((acc) => acc.did === this.activeDid);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers["Authorization"] = "Bearer " + (await accessToken(identity));
|
||||||
|
} else {
|
||||||
|
// it's OK without auth... we just won't get any identifiers
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
openDialog(giver) {
|
||||||
|
this.$refs.customDialog.open(giver);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDialogResult(result) {
|
||||||
|
if (result.action === "confirm") {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.recordGive(result.contact?.did, result.description, result.hours);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// action was "cancel" so do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param giverDid may be null
|
||||||
|
* @param description may be an empty string
|
||||||
|
* @param hours may be 0
|
||||||
|
*/
|
||||||
|
public async recordGive(giverDid, description, hours) {
|
||||||
|
if (!this.activeDid) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identity before you can record a give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!description && !hours) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must enter a description or some number of hours.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
const result = await createAndSubmitGive(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
identity,
|
||||||
|
giverDid,
|
||||||
|
this.activeDid,
|
||||||
|
description,
|
||||||
|
hours,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isGiveCreationError(result)) {
|
||||||
|
const errorMessage = getGiveCreationErrorMessage(result);
|
||||||
|
console.log("Error with give result:", result);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage || "There was an error recording the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: "That gift was recorded.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error with give caught:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
getGiveErrorMessage(error) ||
|
||||||
|
"There was an error recording the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setAlert(title, message) {
|
||||||
|
this.alertTitle = title;
|
||||||
|
this.alertMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for readability
|
||||||
|
|
||||||
|
isGiveCreationError(result) {
|
||||||
|
return result.status !== 201 || result.data?.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGiveCreationErrorMessage(result) {
|
||||||
|
return result.data?.error?.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGiveErrorMessage(error) {
|
||||||
|
return error.userMessage || error.response?.data?.error?.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -37,6 +37,8 @@ import QuickNav from "@/components/QuickNav";
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
alertTitle = "";
|
||||||
|
alertMessage = "";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -50,6 +52,29 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
@@ -60,13 +85,17 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
this.alertMessage = "You have no identity yet.";
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "",
|
||||||
|
text: "You have no identity yet.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicKeyHex = identity.keys[0].publicKeyHex;
|
const publicKeyHex = identity.keys[0].publicKeyHex;
|
||||||
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
||||||
const contactInfo = {
|
const contactInfo = {
|
||||||
@@ -91,9 +120,5 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.qrValue = viewPrefix + vcJwt;
|
this.qrValue = viewPrefix + vcJwt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
alertTitle = "";
|
|
||||||
alertMessage = "";
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,82 +14,76 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form>
|
<h3 class="text-sm uppercase font-semibold mb-2">Scan a QR Code…</h3>
|
||||||
<h3 class="text-sm uppercase font-semibold mb-2">Scan a QR Code…</h3>
|
<div class="bg-black rounded overflow-hidden relative mb-4">
|
||||||
<div class="bg-black rounded overflow-hidden relative mb-4">
|
<img src="https://picsum.photos/400/400?random=1" class="w-full" />
|
||||||
<img src="https://picsum.photos/400/400?random=1" class="w-full" />
|
|
||||||
|
|
||||||
<!-- Darken overlay -->
|
<!-- Darken overlay -->
|
||||||
<!-- Top -->
|
<!-- Top -->
|
||||||
<div class="absolute top-0 left-0 right-0 bg-black/50 h-1/4"></div>
|
<div class="absolute top-0 left-0 right-0 bg-black/50 h-1/4"></div>
|
||||||
<!-- Reft -->
|
<!-- Reft -->
|
||||||
<div class="absolute top-1/4 bottom-1/4 left-0 bg-black/50 w-1/4"></div>
|
<div class="absolute top-1/4 bottom-1/4 left-0 bg-black/50 w-1/4"></div>
|
||||||
<!-- Right -->
|
<!-- Right -->
|
||||||
<div
|
<div class="absolute top-1/4 bottom-1/4 right-0 bg-black/50 w-1/4"></div>
|
||||||
class="absolute top-1/4 bottom-1/4 right-0 bg-black/50 w-1/4"
|
<!-- Bottom -->
|
||||||
></div>
|
<div class="absolute bottom-0 left-0 right-0 bg-black/50 h-1/4"></div>
|
||||||
<!-- Bottom -->
|
|
||||||
<div class="absolute bottom-0 left-0 right-0 bg-black/50 h-1/4"></div>
|
|
||||||
|
|
||||||
<!-- Reticle overlay -->
|
<!-- Reticle overlay -->
|
||||||
<!-- Top-left -->
|
<!-- Top-left -->
|
||||||
<div
|
<div
|
||||||
class="absolute top-1/4 left-1/4 h-6 w-6 border-white border-t-4 border-l-4 drop-shadow"
|
class="absolute top-1/4 left-1/4 h-6 w-6 border-white border-t-4 border-l-4 drop-shadow"
|
||||||
></div>
|
></div>
|
||||||
<!-- Top-right -->
|
<!-- Top-right -->
|
||||||
<div
|
<div
|
||||||
class="absolute top-1/4 right-1/4 h-6 w-6 border-white border-t-4 border-r-4 drop-shadow"
|
class="absolute top-1/4 right-1/4 h-6 w-6 border-white border-t-4 border-r-4 drop-shadow"
|
||||||
></div>
|
></div>
|
||||||
<!-- Bottom-left -->
|
<!-- Bottom-left -->
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-1/4 left-1/4 h-6 w-6 border-white border-b-4 border-l-4 drop-shadow"
|
class="absolute bottom-1/4 left-1/4 h-6 w-6 border-white border-b-4 border-l-4 drop-shadow"
|
||||||
></div>
|
></div>
|
||||||
<!-- Bottom-right -->
|
<!-- Bottom-right -->
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-1/4 right-1/4 h-6 w-6 border-white border-b-4 border-r-4 drop-shadow"
|
class="absolute bottom-1/4 right-1/4 h-6 w-6 border-white border-b-4 border-r-4 drop-shadow"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-2">
|
<h3 class="text-sm uppercase font-semibold mb-2">…or Enter Contact Data</h3>
|
||||||
…or Enter Contact Data
|
<input
|
||||||
</h3>
|
type="text"
|
||||||
|
placeholder="Name (optional)"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="ID"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Public Key (optional)"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="submit"
|
||||||
placeholder="Name (optional)"
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
value="Look Up Contact"
|
||||||
/>
|
/>
|
||||||
<input
|
<button
|
||||||
type="text"
|
type="button"
|
||||||
placeholder="ID"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
>
|
||||||
/>
|
Cancel
|
||||||
<input
|
</button>
|
||||||
type="text"
|
</div>
|
||||||
placeholder="Public Key (optional)"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="mt-8">
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
|
||||||
value="Look Up Contact"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ContactScanView extends Vue {}
|
export default class ContactScanView extends Vue {}
|
||||||
|
|||||||
@@ -66,18 +66,27 @@
|
|||||||
: "Unconfirmed"
|
: "Unconfirmed"
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
|
<br />
|
||||||
|
(Only hours shown)
|
||||||
|
<br />
|
||||||
|
(Only recent shown)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul class="">
|
<ul v-if="contacts.length > 0" class="border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300"
|
class="border-b border-slate-300 pt-2.5 pb-4"
|
||||||
v-for="contact in contacts"
|
v-for="contact in contacts"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
>
|
>
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<h2 class="text-base font-semibold">
|
<h2 class="text-base font-semibold">
|
||||||
|
<EntityIcon
|
||||||
|
:entityId="contact.did"
|
||||||
|
:iconSize="24"
|
||||||
|
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||||
|
></EntityIcon>
|
||||||
{{ contact.name || "(no name)" }}
|
{{ contact.name || "(no name)" }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="text-sm truncate">{{ contact.did }}</div>
|
<div class="text-sm truncate">{{ contact.did }}</div>
|
||||||
@@ -85,70 +94,85 @@
|
|||||||
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
||||||
v-if="contact.seesMe"
|
<button
|
||||||
class="tooltip"
|
v-if="contact.seesMe"
|
||||||
@click="setVisibility(contact, false)"
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
>
|
@click="setVisibility(contact, false)"
|
||||||
<fa icon="eye" class="text-slate-900 fa-fw ml-1" />
|
title="They can see you"
|
||||||
<span class="tooltiptext">They can see you</span>
|
>
|
||||||
</button>
|
<fa icon="eye" class="fa-fw" />
|
||||||
<button v-else class="tooltip" @click="setVisibility(contact, true)">
|
</button>
|
||||||
<span class="tooltiptext">They cannot see you</span>
|
<button
|
||||||
<fa icon="eye-slash" class="text-slate-900 fa-fw ml-1" />
|
v-else
|
||||||
</button>
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
|
@click="setVisibility(contact, true)"
|
||||||
|
title="They cannot see you"
|
||||||
|
>
|
||||||
|
<fa icon="eye-slash" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button class="tooltip" @click="checkVisibility(contact)">
|
<button
|
||||||
<span class="tooltiptext">Check Visibility</span>
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
<fa icon="rotate" class="text-slate-900 fa-fw ml-1" />
|
@click="checkVisibility(contact)"
|
||||||
</button>
|
title="Check Visibility"
|
||||||
|
>
|
||||||
|
<fa icon="rotate" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button v-if="contact.registered" class="tooltip">
|
<button
|
||||||
<span class="tooltiptext">Registered</span>
|
v-if="contact.registered"
|
||||||
<fa icon="person-circle-check" class="text-slate-900 fa-fw ml-1" />
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
</button>
|
title="Registered"
|
||||||
<button v-else @click="register(contact)" class="tooltip">
|
>
|
||||||
<span class="tooltiptext">Registration Unknown</span>
|
<fa icon="person-circle-check" class="fa-fw" />
|
||||||
<fa
|
</button>
|
||||||
icon="person-circle-question"
|
<button
|
||||||
class="text-slate-900 fa-fw ml-1"
|
v-else
|
||||||
/>
|
@click="register(contact)"
|
||||||
</button>
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
|
title="Registration unknown"
|
||||||
|
>
|
||||||
|
<fa icon="person-circle-question" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button @click="deleteContact(contact)" class="px-9 tooltip">
|
<button
|
||||||
<span class="tooltiptext">Delete!</span>
|
@click="deleteContact(contact)"
|
||||||
<fa icon="trash-can" class="text-red-600 fa-fw ml-1" />
|
class="text-sm uppercase bg-red-600 text-white px-2 py-1.5 rounded-md"
|
||||||
</button>
|
title="Delete"
|
||||||
|
>
|
||||||
|
<fa icon="trash-can" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<div v-if="showGiveNumbers" class="float-right">
|
<div
|
||||||
<div class="float-right">
|
v-if="showGiveNumbers && contact.did != activeDid"
|
||||||
<div class="tooltip">
|
class="ml-auto flex gap-1.5"
|
||||||
to:
|
>
|
||||||
|
<button
|
||||||
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
|
||||||
|
@click="onClickAddGive(activeDid, contact.did)"
|
||||||
|
title="givenByMeDescriptions[contact.did]"
|
||||||
|
>
|
||||||
|
To:
|
||||||
{{
|
{{
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
this.showGiveTotals
|
this.showGiveTotals
|
||||||
? ((givenByMeConfirmed[contact.did] || 0)
|
? ((givenByMeConfirmed[contact.did] || 0)
|
||||||
+ (givenByMeUnconfirmed[contact.did] || 0))
|
+ (givenByMeUnconfirmed[contact.did] || 0))
|
||||||
: this.showGiveConfirmed
|
: this.showGiveConfirmed
|
||||||
? (givenByMeConfirmed[contact.did] || 0)
|
? (givenByMeConfirmed[contact.did] || 0)
|
||||||
: (givenByMeUnconfirmed[contact.did] || 0)
|
: (givenByMeUnconfirmed[contact.did] || 0)
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
}}
|
}}
|
||||||
<span
|
<fa icon="plus" />
|
||||||
v-if="givenByMeDescriptions[contact.did]"
|
</button>
|
||||||
class="tooltiptext-left"
|
|
||||||
>
|
<button
|
||||||
{{ givenByMeDescriptions[contact.did] }}
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
|
||||||
</span>
|
@click="onClickAddGive(contact.did, activeDid)"
|
||||||
<button
|
title="givenToMeDescriptions[contact.did]"
|
||||||
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
>
|
||||||
@click="onClickAddGive(activeDid, contact.did)"
|
From:
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip px-2">
|
|
||||||
from:
|
|
||||||
{{
|
{{
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
this.showGiveTotals
|
this.showGiveTotals
|
||||||
@@ -159,34 +183,25 @@
|
|||||||
: (givenToMeUnconfirmed[contact.did] || 0)
|
: (givenToMeUnconfirmed[contact.did] || 0)
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
}}
|
}}
|
||||||
<span
|
<fa icon="plus" />
|
||||||
v-if="givenToMeDescriptions[contact.did]"
|
</button>
|
||||||
class="tooltiptext-left"
|
|
||||||
>
|
|
||||||
{{ givenToMeDescriptions[contact.did] }}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
|
||||||
@click="onClickAddGive(contact.did, activeDid)"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'contact-amounts',
|
name: 'contact-amounts',
|
||||||
query: { contactDid: contact.did },
|
query: { contactDid: contact.did },
|
||||||
}"
|
}"
|
||||||
class="tooltip"
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
|
title="See all given activity"
|
||||||
>
|
>
|
||||||
<fa icon="file-lines" class="text-slate-600 fa-fw ml-1" />
|
<fa icon="file-lines" class="fa-fw" />
|
||||||
<span class="tooltiptext-left">See All Given Activity</span>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p v-else>This identity has no contacts.</p>
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
:alertTitle="alertTitle"
|
:alertTitle="alertTitle"
|
||||||
:alertMessage="alertMessage"
|
:alertMessage="alertMessage"
|
||||||
@@ -199,26 +214,25 @@ import { AxiosError } from "axios";
|
|||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
GiveServerRecord,
|
|
||||||
GiveVerifiableCredential,
|
GiveVerifiableCredential,
|
||||||
RegisterVerifiableCredential,
|
RegisterVerifiableCredential,
|
||||||
SERVICE_ID,
|
SERVICE_ID,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { AlertMessage, QuickNav },
|
components: { AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ContactsView extends Vue {
|
export default class ContactsView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
@@ -242,8 +256,9 @@ export default class ContactsView extends Vue {
|
|||||||
showGiveNumbers = false;
|
showGiveNumbers = false;
|
||||||
showGiveTotals = true;
|
showGiveTotals = true;
|
||||||
showGiveConfirmed = true;
|
showGiveConfirmed = true;
|
||||||
|
alertTitle = "";
|
||||||
|
alertMessage = "";
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
@@ -257,126 +272,140 @@ export default class ContactsView extends Vue {
|
|||||||
const allContacts = await db.contacts.toArray();
|
const allContacts = await db.contacts.toArray();
|
||||||
this.contacts = R.sort(
|
this.contacts = R.sort(
|
||||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||||
allContacts
|
allContacts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadGives() {
|
public async getIdentity(activeDid) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
console.error(
|
throw new Error(
|
||||||
"Attempted to load Give records with no identity available."
|
"Attempted to load Give records with no identity available.",
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeadersAndIdentity(activeDid) {
|
||||||
|
const identity = await this.getIdentity(activeDid);
|
||||||
|
const headers = await this.getHeaders(identity);
|
||||||
|
|
||||||
|
return { headers, identity };
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadGives() {
|
||||||
|
const handleResponse = (
|
||||||
|
resp,
|
||||||
|
descriptions,
|
||||||
|
confirmed,
|
||||||
|
unconfirmed,
|
||||||
|
useRecipient,
|
||||||
|
) => {
|
||||||
|
if (resp.status === 200) {
|
||||||
|
const allData = resp.data.data;
|
||||||
|
for (const give of allData) {
|
||||||
|
const otherDid = useRecipient ? give.recipientDid : give.agentDid;
|
||||||
|
if (give.unit === "HUR") {
|
||||||
|
if (give.amountConfirmed) {
|
||||||
|
const prevAmount = confirmed[otherDid] || 0;
|
||||||
|
confirmed[otherDid] = prevAmount + give.amount;
|
||||||
|
} else {
|
||||||
|
const prevAmount = unconfirmed[otherDid] || 0;
|
||||||
|
unconfirmed[otherDid] = prevAmount + give.amount;
|
||||||
|
}
|
||||||
|
if (!descriptions[otherDid] && give.description) {
|
||||||
|
descriptions[otherDid] = give.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Got bad response status & data of",
|
||||||
|
resp.status,
|
||||||
|
resp.data,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text:
|
||||||
|
"Got an error retrieving your " +
|
||||||
|
resp.config.url.includes("recipientDid")
|
||||||
|
? "received"
|
||||||
|
: "given" + " time from the server.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// load all the time I have given
|
|
||||||
try {
|
try {
|
||||||
const url =
|
const { headers } = await this.getHeadersAndIdentity(this.activeDid);
|
||||||
|
const givenByUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/gives?agentDid=" +
|
"/api/v2/report/gives?agentDid=" +
|
||||||
encodeURIComponent(identity.did);
|
encodeURIComponent(this.activeDid);
|
||||||
const token = await accessToken(identity);
|
const givenToUrl =
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
const resp = await this.axios.get(url, { headers });
|
|
||||||
//console.log("All gifts you've given:", resp.data);
|
|
||||||
if (resp.status === 200) {
|
|
||||||
const contactDescriptions: Record<string, string> = {};
|
|
||||||
const contactConfirmed: Record<string, number> = {};
|
|
||||||
const contactUnconfirmed: Record<string, number> = {};
|
|
||||||
const allData: Array<GiveServerRecord> = resp.data.data;
|
|
||||||
for (const give of allData) {
|
|
||||||
if (give.unit == "HUR") {
|
|
||||||
const recipDid: string = give.recipientDid;
|
|
||||||
if (give.amountConfirmed) {
|
|
||||||
const prevAmount = contactConfirmed[recipDid] || 0;
|
|
||||||
contactConfirmed[recipDid] = prevAmount + give.amount;
|
|
||||||
} else {
|
|
||||||
const prevAmount = contactUnconfirmed[recipDid] || 0;
|
|
||||||
contactUnconfirmed[recipDid] = prevAmount + give.amount;
|
|
||||||
}
|
|
||||||
if (!contactDescriptions[recipDid] && give.description) {
|
|
||||||
// Since many make the tooltip too big, we'll just use the latest.
|
|
||||||
contactDescriptions[recipDid] = give.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//console.log("Done retrieving gives", contactConfirmed);
|
|
||||||
this.givenByMeDescriptions = contactDescriptions;
|
|
||||||
this.givenByMeConfirmed = contactConfirmed;
|
|
||||||
this.givenByMeUnconfirmed = contactUnconfirmed;
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Got bad response status & data of",
|
|
||||||
resp.status,
|
|
||||||
resp.data
|
|
||||||
);
|
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
this.alertMessage =
|
|
||||||
"Got an error retrieving your given time from the server.";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
this.alertMessage = error as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load all the time I have received
|
|
||||||
try {
|
|
||||||
const url =
|
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/gives?recipientDid=" +
|
"/api/v2/report/gives?recipientDid=" +
|
||||||
encodeURIComponent(identity.did);
|
encodeURIComponent(this.activeDid);
|
||||||
const token = await accessToken(identity);
|
|
||||||
const headers = {
|
const [givenByMeResp, givenToMeResp] = await Promise.all([
|
||||||
"Content-Type": "application/json",
|
this.axios.get(givenByUrl, { headers }),
|
||||||
Authorization: "Bearer " + token,
|
this.axios.get(givenToUrl, { headers }),
|
||||||
};
|
]);
|
||||||
const resp = await this.axios.get(url, { headers });
|
|
||||||
//console.log("All gifts you've recieved:", resp.data);
|
const givenByMeDescriptions = {};
|
||||||
if (resp.status === 200) {
|
const givenByMeConfirmed = {};
|
||||||
const contactDescriptions: Record<string, string> = {};
|
const givenByMeUnconfirmed = {};
|
||||||
const contactConfirmed: Record<string, number> = {};
|
handleResponse(
|
||||||
const contactUnconfirmed: Record<string, number> = {};
|
givenByMeResp,
|
||||||
const allData: Array<GiveServerRecord> = resp.data.data;
|
givenByMeDescriptions,
|
||||||
for (const give of allData) {
|
givenByMeConfirmed,
|
||||||
if (give.unit == "HUR") {
|
givenByMeUnconfirmed,
|
||||||
if (give.amountConfirmed) {
|
true,
|
||||||
const prevAmount = contactConfirmed[give.agentDid] || 0;
|
);
|
||||||
contactConfirmed[give.agentDid] = prevAmount + give.amount;
|
this.givenByMeDescriptions = givenByMeDescriptions;
|
||||||
} else {
|
this.givenByMeConfirmed = givenByMeConfirmed;
|
||||||
const prevAmount = contactUnconfirmed[give.agentDid] || 0;
|
this.givenByMeUnconfirmed = givenByMeUnconfirmed;
|
||||||
contactUnconfirmed[give.agentDid] = prevAmount + give.amount;
|
|
||||||
}
|
const givenToMeDescriptions = {};
|
||||||
if (!contactDescriptions[give.agentDid] && give.description) {
|
const givenToMeConfirmed = {};
|
||||||
// Since many make the tooltip too big, we'll just use the latest.
|
const givenToMeUnconfirmed = {};
|
||||||
contactDescriptions[give.agentDid] = give.description;
|
handleResponse(
|
||||||
}
|
givenToMeResp,
|
||||||
}
|
givenToMeDescriptions,
|
||||||
}
|
givenToMeConfirmed,
|
||||||
//console.log("Done retrieving receipts", contactConfirmed);
|
givenToMeUnconfirmed,
|
||||||
this.givenToMeDescriptions = contactDescriptions;
|
false,
|
||||||
this.givenToMeConfirmed = contactConfirmed;
|
);
|
||||||
this.givenToMeUnconfirmed = contactUnconfirmed;
|
this.givenToMeDescriptions = givenToMeDescriptions;
|
||||||
} else {
|
this.givenToMeConfirmed = givenToMeConfirmed;
|
||||||
console.error(
|
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
||||||
"Got bad response status & data of",
|
|
||||||
resp.status,
|
|
||||||
resp.data
|
|
||||||
);
|
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
this.alertMessage =
|
|
||||||
"Got an error retrieving your received time from the server.";
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = error as string;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: error as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +432,7 @@ export default class ContactsView extends Vue {
|
|||||||
const allContacts = this.contacts.concat([newContact]);
|
const allContacts = this.contacts.concat([newContact]);
|
||||||
this.contacts = R.sort(
|
this.contacts = R.sort(
|
||||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||||
allContacts
|
allContacts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +443,7 @@ export default class ContactsView extends Vue {
|
|||||||
this.nameForDid(this.contacts, contact.did) +
|
this.nameForDid(this.contacts, contact.did) +
|
||||||
" with DID " +
|
" with DID " +
|
||||||
contact.did +
|
contact.did +
|
||||||
" ?"
|
" ?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -428,18 +457,11 @@ export default class ContactsView extends Vue {
|
|||||||
confirm(
|
confirm(
|
||||||
"Are you sure you want to use one of your registrations for " +
|
"Are you sure you want to use one of your registrations for " +
|
||||||
this.nameForDid(this.contacts, contact.did) +
|
this.nameForDid(this.contacts, contact.did) +
|
||||||
"?"
|
"?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a claim
|
|
||||||
const vcClaim: RegisterVerifiableCredential = {
|
const vcClaim: RegisterVerifiableCredential = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "RegisterAction",
|
"@type": "RegisterAction",
|
||||||
@@ -471,28 +493,37 @@ export default class ContactsView extends Vue {
|
|||||||
// Make the xhr request payload
|
// Make the xhr request payload
|
||||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||||
const url = this.apiServer + "/api/v2/claim";
|
const url = this.apiServer + "/api/v2/claim";
|
||||||
const token = await accessToken(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
//console.log("Got resp data:", resp.data);
|
|
||||||
if (resp.data?.success?.embeddedRecordError) {
|
if (resp.data?.success?.embeddedRecordError) {
|
||||||
this.alertTitle = "Registration Still Unknown";
|
|
||||||
let message = "There was some problem with the registration.";
|
let message = "There was some problem with the registration.";
|
||||||
if (typeof resp.data.success.embeddedRecordError == "string") {
|
if (typeof resp.data.success.embeddedRecordError == "string") {
|
||||||
message += " " + resp.data.success.embeddedRecordError;
|
message += " " + resp.data.success.embeddedRecordError;
|
||||||
}
|
}
|
||||||
this.alertMessage = message;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Registration Still Unknown",
|
||||||
|
text: message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else if (resp.data?.success?.handleId) {
|
} else if (resp.data?.success?.handleId) {
|
||||||
contact.registered = true;
|
contact.registered = true;
|
||||||
db.contacts.update(contact.did, { registered: true });
|
db.contacts.update(contact.did, { registered: true });
|
||||||
|
|
||||||
this.alertTitle = "Registration Success";
|
this.$notify(
|
||||||
this.alertMessage = contact.name + " has been registered.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Registration Success",
|
||||||
|
text: contact.name + " has been registered.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let userMessage = "There was an error. See logs for more info.";
|
let userMessage = "There was an error. See logs for more info.";
|
||||||
@@ -507,8 +538,15 @@ export default class ContactsView extends Vue {
|
|||||||
userMessage = error as string;
|
userMessage = error as string;
|
||||||
}
|
}
|
||||||
// Now set that error for the user to see.
|
// Now set that error for the user to see.
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = userMessage;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -519,19 +557,8 @@ export default class ContactsView extends Vue {
|
|||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/report/" +
|
"/api/report/" +
|
||||||
(visibility ? "canSeeMe" : "cannotSeeMe");
|
(visibility ? "canSeeMe" : "cannotSeeMe");
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const headers = await this.getHeaders(identity);
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await accessToken(identity);
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
const payload = JSON.stringify({ did: contact.did });
|
const payload = JSON.stringify({ did: contact.did });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -540,17 +567,39 @@ export default class ContactsView extends Vue {
|
|||||||
contact.seesMe = visibility;
|
contact.seesMe = visibility;
|
||||||
db.contacts.update(contact.did, { seesMe: visibility });
|
db.contacts.update(contact.did, { seesMe: visibility });
|
||||||
} else {
|
} else {
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
console.error("Bad response setting visibility: ", resp.data);
|
console.error("Bad response setting visibility: ", resp.data);
|
||||||
if (resp.data.error?.message) {
|
if (resp.data.error?.message) {
|
||||||
this.alertMessage = resp.data.error?.message;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: resp.data.error?.message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.alertMessage = "Bad server response of " + resp.status;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Bad server response of " + resp.status,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = err as string;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: err as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,19 +608,6 @@ export default class ContactsView extends Vue {
|
|||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/report/canDidExplicitlySeeMe?did=" +
|
"/api/report/canDidExplicitlySeeMe?did=" +
|
||||||
encodeURIComponent(contact.did);
|
encodeURIComponent(contact.did);
|
||||||
await accountsDB.open();
|
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await accessToken(identity);
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
@@ -580,23 +616,52 @@ export default class ContactsView extends Vue {
|
|||||||
contact.seesMe = visibility;
|
contact.seesMe = visibility;
|
||||||
db.contacts.update(contact.did, { seesMe: visibility });
|
db.contacts.update(contact.did, { seesMe: visibility });
|
||||||
|
|
||||||
this.alertTitle = "Refreshed";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
this.nameForContact(contact, true) +
|
group: "alert",
|
||||||
" can " +
|
type: "toast",
|
||||||
(visibility ? "" : "not ") +
|
title: "Refreshed",
|
||||||
"see your activity.";
|
text:
|
||||||
|
this.nameForContact(contact, true) +
|
||||||
|
" can " +
|
||||||
|
(visibility ? "" : "not ") +
|
||||||
|
"see your activity.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.alertTitle = "Error With Server";
|
|
||||||
if (resp.data.error?.message) {
|
if (resp.data.error?.message) {
|
||||||
this.alertMessage = resp.data.error?.message;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: resp.data.error?.message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.alertMessage = "Bad server response of " + resp.status;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: "Bad server response of " + resp.status,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = err as string;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: err as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,13 +681,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
|
async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
|
||||||
await accountsDB.open();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// if they have unconfirmed amounts, ask to confirm those first
|
// if they have unconfirmed amounts, ask to confirm those first
|
||||||
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
|
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
|
||||||
@@ -631,7 +690,7 @@ export default class ContactsView extends Vue {
|
|||||||
"There are " +
|
"There are " +
|
||||||
this.givenToMeUnconfirmed[fromDid] +
|
this.givenToMeUnconfirmed[fromDid] +
|
||||||
" unconfirmed hours from them." +
|
" unconfirmed hours from them." +
|
||||||
" Would you like to confirm some of those hours?"
|
" Would you like to confirm some of those hours?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
@@ -641,15 +700,35 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.isNumeric(this.hourInput)) {
|
if (!this.isNumeric(this.hourInput)) {
|
||||||
this.alertTitle = "Input Error";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
"This is not a valid number of hours: " + this.hourInput;
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Input Error",
|
||||||
|
text: "This is not a valid number of hours: " + this.hourInput,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else if (!parseFloat(this.hourInput)) {
|
} else if (!parseFloat(this.hourInput)) {
|
||||||
this.alertTitle = "Input Error";
|
this.$notify(
|
||||||
this.alertMessage = "Giving 0 hours does nothing.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Input Error",
|
||||||
|
text: "Giving 0 hours does nothing.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else if (!identity) {
|
} else if (!identity) {
|
||||||
this.alertTitle = "Status Error";
|
this.$notify(
|
||||||
this.alertMessage = "No identity is available.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Status Error",
|
||||||
|
text: "No identity is available.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// ask to confirm amount
|
// ask to confirm amount
|
||||||
let toFrom;
|
let toFrom;
|
||||||
@@ -671,7 +750,7 @@ export default class ContactsView extends Vue {
|
|||||||
" hours " +
|
" hours " +
|
||||||
toFrom +
|
toFrom +
|
||||||
description +
|
description +
|
||||||
"?"
|
"?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.createAndSubmitGive(
|
this.createAndSubmitGive(
|
||||||
@@ -679,7 +758,7 @@ export default class ContactsView extends Vue {
|
|||||||
fromDid,
|
fromDid,
|
||||||
toDid,
|
toDid,
|
||||||
parseFloat(this.hourInput),
|
parseFloat(this.hourInput),
|
||||||
this.hourDescriptionInput
|
this.hourDescriptionInput,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -690,7 +769,7 @@ export default class ContactsView extends Vue {
|
|||||||
fromDid: string,
|
fromDid: string,
|
||||||
toDid: string,
|
toDid: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
description: string
|
description: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim: GiveVerifiableCredential = {
|
const vcClaim: GiveVerifiableCredential = {
|
||||||
@@ -728,18 +807,20 @@ export default class ContactsView extends Vue {
|
|||||||
|
|
||||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||||
const url = this.apiServer + "/api/v2/claim";
|
const url = this.apiServer + "/api/v2/claim";
|
||||||
const token = await accessToken(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
//console.log("Got resp data:", resp.data);
|
|
||||||
if (resp.data?.success?.handleId) {
|
if (resp.data?.success?.handleId) {
|
||||||
this.alertTitle = "Done";
|
this.$notify(
|
||||||
this.alertMessage = "Successfully logged time to the server.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Done",
|
||||||
|
text: "Successfully logged time to the server.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
|
||||||
if (fromDid === identity.did) {
|
if (fromDid === identity.did) {
|
||||||
const newList = R.clone(this.givenByMeUnconfirmed);
|
const newList = R.clone(this.givenByMeUnconfirmed);
|
||||||
@@ -764,8 +845,15 @@ export default class ContactsView extends Vue {
|
|||||||
userMessage = error as string;
|
userMessage = error as string;
|
||||||
}
|
}
|
||||||
// Now set that error for the user to see.
|
// Now set that error for the user to see.
|
||||||
this.alertTitle = "Error With Server";
|
this.$notify(
|
||||||
this.alertMessage = userMessage;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error With Server",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -783,10 +871,6 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
alertTitle = "";
|
|
||||||
alertMessage = "";
|
|
||||||
|
|
||||||
public showGiveAmountsClassNames() {
|
public showGiveAmountsClassNames() {
|
||||||
return {
|
return {
|
||||||
"bg-slate-500": this.showGiveTotals,
|
"bg-slate-500": this.showGiveTotals,
|
||||||
|
|||||||
@@ -30,87 +30,80 @@
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="inline-block py-3 rounded-t-lg border-b-2 active text-blue-600 border-blue-600 font-semibold"
|
@click="
|
||||||
|
projects = [];
|
||||||
|
searchLocal();
|
||||||
|
"
|
||||||
|
v-bind:class="computedLocalTabClassNames()"
|
||||||
>
|
>
|
||||||
Nearby
|
Nearby
|
||||||
<span
|
<span
|
||||||
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||||
>20+</span
|
>{{ localCount }}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="inline-block py-3 rounded-t-lg border-b-2 border-transparent hover:text-slate-600 hover:border-slate-300"
|
v-bind:class="computedRemoteTabClassNames()"
|
||||||
|
@click="
|
||||||
|
projects = [];
|
||||||
|
search();
|
||||||
|
"
|
||||||
>
|
>
|
||||||
Remote
|
Remote
|
||||||
<span
|
<span
|
||||||
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||||
>13</span
|
>{{ remoteCount }}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Animation -->
|
||||||
|
<div
|
||||||
|
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||||
|
v-if="isLoading"
|
||||||
|
>
|
||||||
|
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul class="">
|
<InfiniteScroll @reached-bottom="loadMoreData">
|
||||||
<li class="border-b border-slate-300">
|
<ul>
|
||||||
<a href="project-view.html" class="block py-4 flex gap-4">
|
<li
|
||||||
<div class="w-12">
|
class="border-b border-slate-300"
|
||||||
<img
|
v-for="project in projects"
|
||||||
src="https://picsum.photos/200/200?random=1"
|
:key="project.handleId"
|
||||||
class="w-full rounded"
|
>
|
||||||
/>
|
<a
|
||||||
</div>
|
@click="onClickLoadProject(project.handleId)"
|
||||||
|
class="block py-4 flex gap-4"
|
||||||
<div class="grow">
|
>
|
||||||
<h2 class="text-base font-semibold">Canyon cleanup</h2>
|
<div class="w-12">
|
||||||
<div class="text-sm">
|
<EntityIcon
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa> Rotary
|
:entityId="project.handleId"
|
||||||
|
:iconSize="48"
|
||||||
|
class="block border border-slate-300 rounded-md"
|
||||||
|
></EntityIcon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="border-b border-slate-300">
|
<div class="grow">
|
||||||
<a href="project-view.html" class="block py-4 flex gap-4">
|
<h2 class="text-base font-semibold">{{ project.name }}</h2>
|
||||||
<div class="w-12">
|
<h2 class="text-base font-semibold">{{ project.name }}</h2>
|
||||||
<img
|
<div class="text-sm">
|
||||||
src="https://picsum.photos/200/200?random=2"
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
class="w-full rounded"
|
{{
|
||||||
/>
|
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
|
||||||
</div>
|
}}
|
||||||
|
</div>
|
||||||
<div class="grow">
|
|
||||||
<h2 class="text-base font-semibold">Potluck with neighbors</h2>
|
|
||||||
<div class="text-sm">
|
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa> Andrew A.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
|
</InfiniteScroll>
|
||||||
<li class="border-b border-slate-300">
|
|
||||||
<a href="project-view.html" class="block py-4 flex gap-4">
|
|
||||||
<div class="w-12">
|
|
||||||
<img
|
|
||||||
src="https://picsum.photos/200/200?random=3"
|
|
||||||
class="w-full rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grow">
|
|
||||||
<h2 class="text-base font-semibold">Historical site</h2>
|
|
||||||
<div class="text-sm">
|
|
||||||
<fa icon="user" class="fa-fw text-slate-400 mr-1"></fa>
|
|
||||||
<em>Unknown</em>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
:alertTitle="alertTitle"
|
:alertTitle="alertTitle"
|
||||||
:alertMessage="alertMessage"
|
:alertMessage="alertMessage"
|
||||||
@@ -122,74 +115,267 @@
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import * as R from "ramda";
|
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import { didInfo } from "@/libs/endorserServer";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { AlertMessage, QuickNav },
|
components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {
|
export default class DiscoverView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
searchTerms = "";
|
searchTerms = "";
|
||||||
|
alertMessage = "";
|
||||||
|
alertTitle = "";
|
||||||
|
projects: ProjectData[] = [];
|
||||||
|
isLocalActive = true;
|
||||||
|
isRemoteActive = false;
|
||||||
|
localCount = 0;
|
||||||
|
remoteCount = 0;
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
|
// make this function available to the Vue template
|
||||||
|
didInfo = didInfo;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
|
this.searchLocal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async search() {
|
public async buildHeaders() {
|
||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
if (this.activeDid) {
|
if (this.activeDid) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const allAccounts = await accountsDB.accounts.toArray();
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, allAccounts);
|
const account = allAccounts.find((acc) => acc.did === this.activeDid);
|
||||||
//console.log("about to parse from", this.activeDid, account?.identity);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error("No identity found.");
|
throw new Error(
|
||||||
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const token = await accessToken(identity);
|
|
||||||
headers["Authorization"] = "Bearer " + token;
|
headers["Authorization"] = "Bearer " + (await accessToken(identity));
|
||||||
} else {
|
} else {
|
||||||
// it's OK without auth... we just won't get any identifiers
|
// it's OK without auth... we just won't get any identifiers
|
||||||
}
|
}
|
||||||
const claimContents =
|
return headers;
|
||||||
"claimContents=" + encodeURIComponent(this.searchTerms);
|
|
||||||
const claimType = "claimType=PlanAction";
|
|
||||||
const queryParams = [claimContents, claimType].join("&");
|
|
||||||
return fetch(this.apiServer + "/api/v2/report/claims?" + queryParams, {
|
|
||||||
method: "GET",
|
|
||||||
headers: headers,
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (response.status !== 200) {
|
|
||||||
const details = await response.text();
|
|
||||||
throw details;
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((results) => {
|
|
||||||
if (results.data) {
|
|
||||||
console.log(results.data);
|
|
||||||
} else {
|
|
||||||
throw JSON.stringify(results);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.log("Error with feed load:", e);
|
|
||||||
this.alertMessage =
|
|
||||||
e.userMessage || "There was an error retrieving projects.";
|
|
||||||
this.alertTitle = "Error";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alertMessage = "";
|
public async search(beforeId?: string) {
|
||||||
alertTitle = "";
|
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
|
||||||
|
|
||||||
|
if (beforeId) {
|
||||||
|
queryParams = queryParams + `&beforeId=${beforeId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRemoteActive = true;
|
||||||
|
this.isLocalActive = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
const response = await fetch(
|
||||||
|
this.apiServer + "/api/v2/report/plans?" + queryParams,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await this.buildHeaders(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
const details = await response.text();
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: `There was a problem accessing the server. Please try again later. (${details})`,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw details;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
|
||||||
|
const plans: ProjectData[] = results.data;
|
||||||
|
if (plans) {
|
||||||
|
for (const plan of plans) {
|
||||||
|
const { name, description, handleId, rowid, issuerDid } = plan;
|
||||||
|
this.projects.push({ name, description, handleId, rowid, issuerDid });
|
||||||
|
}
|
||||||
|
this.remoteCount = this.projects.length;
|
||||||
|
} else {
|
||||||
|
throw JSON.stringify(results);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error with feed load:", e);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: e.userMessage || "There was a problem retrieving projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async searchLocal(beforeId?: string) {
|
||||||
|
const claimContents =
|
||||||
|
"claimContents=" + encodeURIComponent(this.searchTerms);
|
||||||
|
let queryParams = [
|
||||||
|
claimContents,
|
||||||
|
"minLocLat=40.901000",
|
||||||
|
"maxLocLat=40.904000",
|
||||||
|
"westLocLon=-111.914000",
|
||||||
|
"eastLocLon=-111.909000",
|
||||||
|
].join("&");
|
||||||
|
|
||||||
|
if (beforeId) {
|
||||||
|
queryParams = queryParams + `&beforeId=${beforeId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.isLocalActive = true;
|
||||||
|
this.isRemoteActive = false;
|
||||||
|
const response = await fetch(
|
||||||
|
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await this.buildHeaders(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
const details = await response.text();
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: `There was a problem accessing the server. Please try again later. (${details})`,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
throw await response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
|
||||||
|
if (results.data) {
|
||||||
|
if (beforeId) {
|
||||||
|
const plans: ProjectData[] = results.data;
|
||||||
|
for (const plan of plans) {
|
||||||
|
const { name, description, handleId = plan.handleId, rowid } = plan;
|
||||||
|
if (beforeId !== plan["rowid"]) {
|
||||||
|
this.projects.push({ name, description, handleId, rowid });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.projects = results.data;
|
||||||
|
}
|
||||||
|
this.localCount = this.projects.length;
|
||||||
|
} else {
|
||||||
|
throw JSON.stringify(results);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error with feed load:", e);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: e.userMessage || "There was a problem retrieving projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data loader used by infinite scroller
|
||||||
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
|
**/
|
||||||
|
async loadMoreData(payload: boolean) {
|
||||||
|
if (this.projects.length > 0 && payload) {
|
||||||
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
|
if (this.isLocalActive) {
|
||||||
|
this.searchLocal(latestProject["rowid"]);
|
||||||
|
} else if (this.isRemoteActive) {
|
||||||
|
this.search(latestProject["rowid"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle clicking on a project entry found in the list
|
||||||
|
* @param id of the project
|
||||||
|
**/
|
||||||
|
onClickLoadProject(id: string) {
|
||||||
|
localStorage.setItem("projectId", id);
|
||||||
|
const route = {
|
||||||
|
name: "project",
|
||||||
|
};
|
||||||
|
this.$router.push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedLocalTabClassNames() {
|
||||||
|
return {
|
||||||
|
"inline-block": true,
|
||||||
|
"py-3": true,
|
||||||
|
"rounded-t-lg": true,
|
||||||
|
"border-b-2": true,
|
||||||
|
active: this.isLocalActive,
|
||||||
|
"text-blue-600": this.isLocalActive,
|
||||||
|
"border-blue-600": this.isLocalActive,
|
||||||
|
"font-semibold": this.isLocalActive,
|
||||||
|
"border-transparent": !this.isLocalActive,
|
||||||
|
"hover:text-slate-600": !this.isLocalActive,
|
||||||
|
"hover:border-slate-300": !this.isLocalActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public computedRemoteTabClassNames() {
|
||||||
|
return {
|
||||||
|
"inline-block": true,
|
||||||
|
"py-3": true,
|
||||||
|
"rounded-t-lg": true,
|
||||||
|
"border-b-2": true,
|
||||||
|
active: this.isRemoteActive,
|
||||||
|
"text-blue-600": this.isRemoteActive,
|
||||||
|
"border-blue-600": this.isRemoteActive,
|
||||||
|
"font-semibold": this.isRemoteActive,
|
||||||
|
"border-transparent": !this.isRemoteActive,
|
||||||
|
"hover:text-slate-600": !this.isRemoteActive,
|
||||||
|
"hover:border-slate-300": !this.isRemoteActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -128,6 +128,14 @@
|
|||||||
key.
|
key.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
||||||
|
<p>
|
||||||
|
Go
|
||||||
|
<router-link to="start" class="text-blue-500">
|
||||||
|
create another identity here.
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
I know there is a record from someone, so why can't I see that info?
|
I know there is a record from someone, so why can't I see that info?
|
||||||
</h2>
|
</h2>
|
||||||
@@ -146,14 +154,6 @@
|
|||||||
page.
|
page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
|
||||||
<p>
|
|
||||||
Go
|
|
||||||
<router-link to="start" class="text-blue-500">
|
|
||||||
create another identity here.
|
|
||||||
</router-link>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
||||||
<p>
|
<p>
|
||||||
See
|
See
|
||||||
|
|||||||
@@ -7,22 +7,130 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-2xl">Quick Action</h1>
|
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
|
||||||
<p>Choose a contact to whom to show appreciation:</p>
|
|
||||||
<!-- similar contact selection code is in multiple places -->
|
<button
|
||||||
<div class="px-4">
|
@click="
|
||||||
<button
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'toast',
|
||||||
|
text: 'I\'m a toast. Don\'t mind me.',
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Toast (self-dismiss)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'info',
|
||||||
|
title: 'Information Alert',
|
||||||
|
text: 'Just wanted you to know.',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'success',
|
||||||
|
title: 'Success Alert',
|
||||||
|
text: 'Congratulations!',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Success
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Warning Alert',
|
||||||
|
text: 'You might wanna look at this.',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Warning
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'alert',
|
||||||
|
type: 'danger',
|
||||||
|
title: 'Danger Alert',
|
||||||
|
text: 'Something terrible has happened!',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="font-bold uppercase bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
|
||||||
|
>
|
||||||
|
Danger
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-bold">Quick Action</h2>
|
||||||
|
<p class="mb-4">Show appreciation to a contact:</p>
|
||||||
|
|
||||||
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
|
<li
|
||||||
v-for="contact in allContacts"
|
v-for="contact in allContacts"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
@click="openDialog(contact)"
|
@click="openDialog(contact)"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
|
||||||
>
|
>
|
||||||
{{ contact.name }}
|
<EntityIcon
|
||||||
</button>
|
:entityId="contact.did"
|
||||||
<span v-if="allContacts.length > 0"> or </span>
|
:iconSize="64"
|
||||||
<button @click="openDialog()" class="text-blue-500">
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||||
someone not specified
|
></EntityIcon>
|
||||||
</button>
|
<h3
|
||||||
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
{{ contact.name || "(no name)" }}
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||||
|
<router-link
|
||||||
|
v-if="allContacts.length > 7"
|
||||||
|
:to="{ name: 'contact-gives' }"
|
||||||
|
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Show More Contacts…
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<!-- If there are no contacts, show this instead: -->
|
||||||
|
<div
|
||||||
|
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
|
||||||
|
>
|
||||||
|
(No contacts to show.)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,63 +141,59 @@
|
|||||||
>
|
>
|
||||||
</GiftedDialog>
|
</GiftedDialog>
|
||||||
|
|
||||||
<div>
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<h1 class="text-2xl">Latest Activity</h1>
|
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
||||||
<span :class="{ hidden: isHiddenSpinner }">
|
<div :class="{ hidden: isHiddenSpinner }">
|
||||||
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||||
Loading…
|
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
||||||
</span>
|
</p>
|
||||||
<ul>
|
</div>
|
||||||
|
<ul class="border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300 py-2"
|
class="border-b border-slate-300 py-2"
|
||||||
v-for="record in feedData"
|
v-for="record in feedData"
|
||||||
:key="record.jwtId"
|
:key="record.jwtId"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border-b border-dashed border-slate-400 text-orange-400 py-2 mb-2 font-bold uppercase text-sm"
|
class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
|
||||||
v-if="record.jwtId == feedLastViewedId"
|
v-if="record.jwtId == feedLastViewedId"
|
||||||
>
|
>
|
||||||
You've seen all claims below:
|
You've seen all claims below:
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<fa
|
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
|
||||||
icon="gift"
|
|
||||||
class="fa-fw flex-none pt-1 pr-2 text-slate-500"
|
|
||||||
></fa>
|
|
||||||
<!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
|
<!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
|
||||||
<span class="">{{ this.giveDescription(record) }}</span>
|
<span class="">{{ this.giveDescription(record) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<AlertMessage
|
||||||
|
:alertTitle="alertTitle"
|
||||||
|
:alertMessage="alertMessage"
|
||||||
|
></AlertMessage>
|
||||||
</section>
|
</section>
|
||||||
<AlertMessage
|
|
||||||
:alertTitle="alertTitle"
|
|
||||||
:alertMessage="alertMessage"
|
|
||||||
></AlertMessage>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as R from "ramda";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Options, Vue } from "vue-class-component";
|
|
||||||
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import { db, accountsDB } from "@/db";
|
import { db, accountsDB } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
|
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: { GiftedDialog, AlertMessage, QuickNav },
|
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
allAccounts: Array<Account> = [];
|
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
feedAllLoaded = false;
|
feedAllLoaded = false;
|
||||||
feedData = [];
|
feedData = [];
|
||||||
@@ -98,12 +202,44 @@ export default class HomeView extends Vue {
|
|||||||
isHiddenSpinner = true;
|
isHiddenSpinner = true;
|
||||||
alertTitle = "";
|
alertTitle = "";
|
||||||
alertMessage = "";
|
alertMessage = "";
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
this.allAccounts = await accountsDB.accounts.toArray();
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
@@ -112,21 +248,48 @@ export default class HomeView extends Vue {
|
|||||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||||
this.updateAllFeed();
|
this.updateAllFeed();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertTitle = "Error";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
err.userMessage ||
|
group: "alert",
|
||||||
"There was an error retrieving the latest sweet, sweet action.";
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
err.userMessage ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAllFeed = async () => {
|
public async buildHeaders() {
|
||||||
this.isHiddenSpinner = false;
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
|
if (this.activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
const account = allAccounts.find((acc) => acc.did === this.activeDid);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers["Authorization"] = "Bearer " + (await accessToken(identity));
|
||||||
|
} else {
|
||||||
|
// it's OK without auth... we just won't get any identifiers
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateAllFeed() {
|
||||||
|
this.isHiddenSpinner = false;
|
||||||
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
|
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
|
||||||
.then(async (results) => {
|
.then(async (results) => {
|
||||||
if (results.data.length > 0) {
|
if (results.data.length > 0) {
|
||||||
this.feedData = this.feedData.concat(results.data);
|
this.feedData = this.feedData.concat(results.data);
|
||||||
//console.log("Feed data:", this.feedData);
|
|
||||||
this.feedAllLoaded = results.hitLimit;
|
this.feedAllLoaded = results.hitLimit;
|
||||||
this.feedPreviousOldestId =
|
this.feedPreviousOldestId =
|
||||||
results.data[results.data.length - 1].jwtId;
|
results.data[results.data.length - 1].jwtId;
|
||||||
@@ -134,7 +297,6 @@ export default class HomeView extends Vue {
|
|||||||
this.feedLastViewedId == null ||
|
this.feedLastViewedId == null ||
|
||||||
this.feedLastViewedId < results.data[0].jwtId
|
this.feedLastViewedId < results.data[0].jwtId
|
||||||
) {
|
) {
|
||||||
// save it to storage
|
|
||||||
await db.open();
|
await db.open();
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
lastViewedClaimId: results.data[0].jwtId,
|
lastViewedClaimId: results.data[0].jwtId,
|
||||||
@@ -145,70 +307,70 @@ export default class HomeView extends Vue {
|
|||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log("Error with feed load:", e);
|
console.log("Error with feed load:", e);
|
||||||
this.alertMessage =
|
this.$notify(
|
||||||
e.userMessage || "There was an error retrieving feed data.";
|
{
|
||||||
this.alertTitle = "Error";
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Export Error",
|
||||||
|
text: e.userMessage || "There was an error retrieving feed data.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isHiddenSpinner = true;
|
this.isHiddenSpinner = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
retrieveClaims = async (endorserApiServer, identifier, beforeId) => {
|
public async retrieveClaims(endorserApiServer, identifier, beforeId) {
|
||||||
//const afterQuery = afterId == null ? "" : "&afterId=" + afterId;
|
|
||||||
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
||||||
const headers = { "Content-Type": "application/json" };
|
const response = await fetch(
|
||||||
if (this.activeDid) {
|
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
||||||
const account = R.find(
|
{
|
||||||
(acc) => acc.did === this.activeDid,
|
method: "GET",
|
||||||
this.allAccounts
|
headers: await this.buildHeaders(),
|
||||||
);
|
},
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
);
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
if (response.status !== 200) {
|
||||||
}
|
throw await response.text();
|
||||||
const token = await accessToken(identity);
|
|
||||||
headers["Authorization"] = "Bearer " + token;
|
|
||||||
} else {
|
|
||||||
// it's OK without auth... we just won't get any identifiers
|
|
||||||
}
|
}
|
||||||
return fetch(this.apiServer + "/api/v2/report/gives?" + beforeQuery, {
|
|
||||||
method: "GET",
|
const results = await response.json();
|
||||||
headers: headers,
|
|
||||||
})
|
if (results.data) {
|
||||||
.then(async (response) => {
|
return results;
|
||||||
if (response.status !== 200) {
|
} else {
|
||||||
const details = await response.text();
|
throw JSON.stringify(results);
|
||||||
throw details;
|
}
|
||||||
}
|
}
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((results) => {
|
|
||||||
if (results.data) {
|
|
||||||
return results;
|
|
||||||
} else {
|
|
||||||
throw JSON.stringify(results);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
giveDescription(giveRecord) {
|
giveDescription(giveRecord) {
|
||||||
let claim = giveRecord.fullClaim;
|
let claim = giveRecord.fullClaim;
|
||||||
if (claim.claim) {
|
if (claim.claim) {
|
||||||
// it's probably a Verified Credential
|
|
||||||
claim = claim.claim;
|
claim = claim.claim;
|
||||||
}
|
}
|
||||||
|
|
||||||
// agent.did is for legacy data, before March 2023
|
// agent.did is for legacy data, before March 2023
|
||||||
const giver =
|
const giverDid =
|
||||||
claim.agent?.identifier || claim.agent?.did || giveRecord.issuer;
|
claim.agent?.identifier || claim.agent?.did;
|
||||||
const giverInfo = didInfo(giver, this.allAccounts, this.allContacts);
|
const giverInfo = didInfo(
|
||||||
|
giverDid,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
);
|
||||||
const gaveAmount = claim.object?.amountOfThisGood
|
const gaveAmount = claim.object?.amountOfThisGood
|
||||||
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||||
: claim.description || "something unknown";
|
: claim.description || "something unknown";
|
||||||
// recipient.did is for legacy data, before March 2023
|
// recipient.did is for legacy data, before March 2023
|
||||||
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
||||||
const gaveRecipientInfo = gaveRecipientId
|
const gaveRecipientInfo = gaveRecipientId
|
||||||
? " to " + didInfo(gaveRecipientId, this.allAccounts, this.allContacts)
|
? " to " +
|
||||||
|
didInfo(
|
||||||
|
gaveRecipientId,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
)
|
||||||
: "";
|
: "";
|
||||||
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
|
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
|
||||||
}
|
}
|
||||||
@@ -224,6 +386,7 @@ export default class HomeView extends Vue {
|
|||||||
openDialog(giver) {
|
openDialog(giver) {
|
||||||
this.$refs.customDialog.open(giver);
|
this.$refs.customDialog.open(giver);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDialogResult(result) {
|
handleDialogResult(result) {
|
||||||
if (result.action === "confirm") {
|
if (result.action === "confirm") {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -241,59 +404,101 @@ export default class HomeView extends Vue {
|
|||||||
* @param description may be an empty string
|
* @param description may be an empty string
|
||||||
* @param hours may be 0
|
* @param hours may be 0
|
||||||
*/
|
*/
|
||||||
recordGive(giverDid, description, hours) {
|
public async recordGive(giverDid, description, hours) {
|
||||||
if (this.activeDid == null) {
|
if (!this.activeDid) {
|
||||||
this.alertTitle = "Error";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
"You must select an identity before you can record a give.";
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identity before you can record a give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description && !hours) {
|
if (!description && !hours) {
|
||||||
this.alertTitle = "Error";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
"You must enter a description or some number of hours.";
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must enter a description or some number of hours.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const account = R.find(
|
|
||||||
(acc) => acc.did === this.activeDid,
|
try {
|
||||||
this.allAccounts
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
);
|
const result = await createAndSubmitGive(
|
||||||
//console.log("about to parse from", this.activeDid, account?.identity);
|
this.axios,
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
this.apiServer,
|
||||||
if (!identity) {
|
identity,
|
||||||
throw new Error("No identity found.");
|
giverDid,
|
||||||
|
this.activeDid,
|
||||||
|
description,
|
||||||
|
hours,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.isGiveCreationError(result)) {
|
||||||
|
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||||
|
console.log("Error with give result:", result);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: errorMessage || "There was an error recording the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: "That gift was recorded.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error with give caught:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
this.getGiveErrorMessage(error) ||
|
||||||
|
"There was an error recording the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
createAndSubmitGive(
|
}
|
||||||
this.axios,
|
|
||||||
this.apiServer,
|
private setAlert(title, message) {
|
||||||
identity,
|
this.alertTitle = title;
|
||||||
giverDid,
|
this.alertMessage = message;
|
||||||
this.activeDid,
|
}
|
||||||
description,
|
|
||||||
hours
|
// Helper functions for readability
|
||||||
)
|
|
||||||
.then((result) => {
|
isGiveCreationError(result) {
|
||||||
if (result.status != 201 || result.data?.error) {
|
return result.status !== 201 || result.data?.error;
|
||||||
console.log("Error with give result:", result);
|
}
|
||||||
this.alertTitle = "Error";
|
|
||||||
this.alertMessage =
|
getGiveCreationErrorMessage(result) {
|
||||||
result.data?.error?.message ||
|
return result.data?.error?.message;
|
||||||
"There was an error recording the give.";
|
}
|
||||||
} else {
|
|
||||||
this.alertTitle = "Success";
|
getGiveErrorMessage(error) {
|
||||||
this.alertMessage = "That gift was recorded.";
|
return error.userMessage || error.response?.data?.error?.message;
|
||||||
//this.updateAllFeed(); // full update is overkill but we should show something
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
// axios throws errors on 400 responses
|
|
||||||
console.log("Error with give caught:", e);
|
|
||||||
this.alertTitle = "Error";
|
|
||||||
this.alertMessage =
|
|
||||||
e.userMessage ||
|
|
||||||
e.response?.data?.error?.message ||
|
|
||||||
"There was an error recording the give.";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
175
src/views/IdentitySwitcherView.vue
Normal file
175
src/views/IdentitySwitcherView.vue
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
<!-- Cancel -->
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'account' }"
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
><fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
Switch Identity
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Identity List -->
|
||||||
|
|
||||||
|
<!-- Current Identity - Display First! -->
|
||||||
|
<div class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4">
|
||||||
|
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
||||||
|
<span class="overflow-hidden">
|
||||||
|
<h2 class="text-xl font-semibold mb-0">
|
||||||
|
{{ firstName }} {{ lastName }}
|
||||||
|
</h2>
|
||||||
|
<div class="text-sm text-slate-500 truncate">
|
||||||
|
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Other Identity/ies -->
|
||||||
|
<ul class="mb-4">
|
||||||
|
<li
|
||||||
|
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
|
||||||
|
v-for="ident in otherIdentities"
|
||||||
|
:key="ident.did"
|
||||||
|
@click="switchAccount(ident.did)"
|
||||||
|
>
|
||||||
|
<fa icon="circle" class="fa-fw text-slate-400 text-xl mr-3"></fa>
|
||||||
|
<span class="overflow-hidden">
|
||||||
|
<h2 class="text-xl font-semibold mb-0"></h2>
|
||||||
|
<div class="text-sm text-slate-500 truncate">
|
||||||
|
<b>ID:</b> <code>{{ ident.did }}</code>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'start' }"
|
||||||
|
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Add Another Identity…
|
||||||
|
</router-link>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
|
||||||
|
@click="switchAccount('0')"
|
||||||
|
>
|
||||||
|
No Identity
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<AlertMessage
|
||||||
|
:alertTitle="alertTitle"
|
||||||
|
:alertMessage="alertMessage"
|
||||||
|
></AlertMessage>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { AppString } from "@/constants/app";
|
||||||
|
import { db, accountsDB } from "@/db";
|
||||||
|
import { AccountsSchema } from "@/db/tables/accounts";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
|
||||||
|
@Component({ components: { AlertMessage, QuickNav } })
|
||||||
|
export default class IdentitySwitcherView extends Vue {
|
||||||
|
Constants = AppString;
|
||||||
|
public accounts: AccountsSchema;
|
||||||
|
public activeDid;
|
||||||
|
public firstName;
|
||||||
|
public lastName;
|
||||||
|
public alertTitle;
|
||||||
|
public alertMessage;
|
||||||
|
public otherIdentities = [];
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.apiServerInput = settings?.apiServer || "";
|
||||||
|
this.firstName = settings?.firstName || "No";
|
||||||
|
this.lastName = settings?.lastName || "Name";
|
||||||
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
|
||||||
|
if (identity) {
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: identity.did,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
for (let n = 0; n < accounts.length; n++) {
|
||||||
|
const did = JSON.parse(accounts[n].identity)["did"];
|
||||||
|
if (did && this.activeDid !== did) {
|
||||||
|
this.otherIdentities.push({ did: did });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err.message ===
|
||||||
|
"Attempted to load account records with no identity available."
|
||||||
|
) {
|
||||||
|
this.limitsMessage = "No identity.";
|
||||||
|
this.loadingLimits = false;
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Creating Account",
|
||||||
|
text: "Clear your cache and start over (after data backup).",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to clear cache at page create because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async switchAccount(did: string) {
|
||||||
|
// 0 means none
|
||||||
|
if (did === "0") {
|
||||||
|
did = undefined;
|
||||||
|
}
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: did,
|
||||||
|
});
|
||||||
|
this.activeDid = did;
|
||||||
|
this.otherIdentities = [];
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
for (let n = 0; n < accounts.length; n++) {
|
||||||
|
const did = JSON.parse(accounts[n].identity)["did"];
|
||||||
|
if (did && this.activeDid !== did) {
|
||||||
|
this.otherIdentities.push({ did: did });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$router.push({ name: "account" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -43,12 +43,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ImportAccountView extends Vue {
|
export default class ImportAccountView extends Vue {
|
||||||
@@ -72,7 +72,7 @@ export default class ImportAccountView extends Vue {
|
|||||||
this.address,
|
this.address,
|
||||||
this.publicHex,
|
this.publicHex,
|
||||||
this.privateHex,
|
this.privateHex,
|
||||||
this.derivationPath
|
this.derivationPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,47 +13,46 @@
|
|||||||
[New/Edit] Identity
|
[New/Edit] Identity
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<form>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="First Name"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
||||||
v-model="firstName"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Last Name"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
||||||
v-model="lastName"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="mt-8">
|
<input
|
||||||
<button
|
type="text"
|
||||||
type="button"
|
placeholder="First Name"
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
@click="onClickSaveChanges()"
|
v-model="firstName"
|
||||||
>
|
/>
|
||||||
Save Changes
|
<input
|
||||||
</button>
|
type="text"
|
||||||
<!-- SHOW ME instead while processing saving changes -->
|
placeholder="Last Name"
|
||||||
<button
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
type="button"
|
v-model="lastName"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
/>
|
||||||
@click="onClickCancel()"
|
|
||||||
>
|
<div class="mt-8">
|
||||||
Cancel
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</div>
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
</form>
|
@click="onClickSaveChanges()"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
<!-- SHOW ME instead while processing saving changes -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
|
@click="onClickCancel()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class NewEditAccountView extends Vue {
|
export default class NewEditAccountView extends Vue {
|
||||||
|
|||||||
@@ -16,54 +16,51 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Project Details -->
|
<!-- Project Details -->
|
||||||
<form>
|
|
||||||
<select
|
<select class="block w-full rounded border border-slate-400 mb-4 px-3 py-2">
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
<option disabled>Choose a commitment type…</option>
|
||||||
|
<option selected>Time</option>
|
||||||
|
<option>Cryptocurrency</option>
|
||||||
|
<option>Money</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Time amount -->
|
||||||
|
<div class="mb-4 flex items-stretch">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="0.0"
|
||||||
|
class="block w-full rounded-l border border-slate-400 px-3 py-2"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="px-4 py-2 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||||
|
>hours</span
|
||||||
>
|
>
|
||||||
<option disabled>Choose a commitment type…</option>
|
</div>
|
||||||
<option selected>Time</option>
|
|
||||||
<option>Cryptocurrency</option>
|
|
||||||
<option>Money</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<!-- Time amount -->
|
<!-- Crypto amount -->
|
||||||
<div class="mb-4 flex items-stretch">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="0.0"
|
|
||||||
class="block w-full rounded-l border border-slate-400 px-3 py-2"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="px-4 py-2 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
|
||||||
>hours</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Crypto amount -->
|
<!-- Money amount -->
|
||||||
|
|
||||||
<!-- Money amount -->
|
<div class="mt-8">
|
||||||
|
<input
|
||||||
<div class="mt-8">
|
type="submit"
|
||||||
<input
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
type="submit"
|
value="Commit"
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
/>
|
||||||
value="Commit"
|
<button
|
||||||
/>
|
type="button"
|
||||||
<button
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
type="button"
|
>
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
Maybe Later
|
||||||
>
|
</button>
|
||||||
Maybe Later
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class NewEditCommitmentView extends Vue {}
|
export default class NewEditCommitmentView extends Vue {}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import * as R from "ramda";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
@@ -83,14 +82,6 @@ import { useAppStore } from "@/store/app";
|
|||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
|
|
||||||
interface VerifiableCredential {
|
|
||||||
"@context": string;
|
|
||||||
"@type": string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
identifier?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { AlertMessage },
|
components: { AlertMessage },
|
||||||
})
|
})
|
||||||
@@ -100,12 +91,44 @@ export default class NewEditProjectView extends Vue {
|
|||||||
projectName = "";
|
projectName = "";
|
||||||
description = "";
|
description = "";
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
|
numAccounts = 0;
|
||||||
|
alertTitle = "";
|
||||||
|
alertMessage = "";
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load project records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
projectId = localStorage.getItem("projectId") || "";
|
projectId = localStorage.getItem("projectId") || "";
|
||||||
isHiddenSave = false;
|
isHiddenSave = false;
|
||||||
isHiddenSpinner = true;
|
isHiddenSpinner = true;
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
@@ -113,16 +136,14 @@ export default class NewEditProjectView extends Vue {
|
|||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
|
||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
await accountsDB.open();
|
if (this.numAccounts === 0) {
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
|
||||||
if (num_accounts === 0) {
|
|
||||||
console.error("Error: no account was found.");
|
console.error("Error: no account was found.");
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error("No identity found.");
|
throw new Error(
|
||||||
|
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.LoadProject(identity);
|
this.LoadProject(identity);
|
||||||
}
|
}
|
||||||
@@ -197,15 +218,15 @@ export default class NewEditProjectView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
// handleId is new in server v release-1.6.0; remove fullIri when that
|
||||||
// version shows up here: https://endorser.ch:3000/api-docs/
|
// version shows up here: https://api.endorser.ch/api-docs/
|
||||||
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
|
||||||
this.errorMessage = "";
|
this.errorMessage = "";
|
||||||
this.alertTitle = "";
|
this.alertTitle = "";
|
||||||
this.alertMessage = "";
|
this.alertMessage = "";
|
||||||
// handleId is new in server v release-1.6.0; remove fullIri when that
|
// handleId is new in server v release-1.6.0; remove fullIri when that
|
||||||
// version shows up here: https://endorser.ch:3000/api-docs/
|
// version shows up here: https://api.endorser.ch/api-docs/
|
||||||
useAppStore().setProjectId(
|
useAppStore().setProjectId(
|
||||||
resp.data.success.handleId || resp.data.success.fullIri
|
resp.data.success.handleId || resp.data.success.fullIri,
|
||||||
);
|
);
|
||||||
setTimeout(
|
setTimeout(
|
||||||
function (that: Vue) {
|
function (that: Vue) {
|
||||||
@@ -215,7 +236,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
that.$router.push(route);
|
that.$router.push(route);
|
||||||
},
|
},
|
||||||
2000,
|
2000,
|
||||||
this
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -224,20 +245,41 @@ export default class NewEditProjectView extends Vue {
|
|||||||
if (serverError) {
|
if (serverError) {
|
||||||
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
||||||
console.log(serverError);
|
console.log(serverError);
|
||||||
this.alertTitle = "User Message";
|
|
||||||
userMessage = serverError.response.data.error.message; // This is info for the user.
|
userMessage = serverError.response.data.error.message; // This is info for the user.
|
||||||
this.alertMessage = userMessage;
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "User Message",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.alertTitle = "Server Message";
|
this.$notify(
|
||||||
this.alertMessage = JSON.stringify(serverError.toJSON());
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Server Message",
|
||||||
|
text: JSON.stringify(serverError.toJSON()),
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
"Here's the full error trying to save the claim:",
|
"Here's the full error trying to save the claim:",
|
||||||
error
|
error,
|
||||||
|
);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Claim Error",
|
||||||
|
text: error as string,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
);
|
);
|
||||||
this.alertTitle = "Claim Error";
|
|
||||||
this.alertMessage = error as string;
|
|
||||||
}
|
}
|
||||||
// Now set that error for the user to see.
|
// Now set that error for the user to see.
|
||||||
this.errorMessage = userMessage;
|
this.errorMessage = userMessage;
|
||||||
@@ -248,17 +290,11 @@ export default class NewEditProjectView extends Vue {
|
|||||||
public async onSaveProjectClick() {
|
public async onSaveProjectClick() {
|
||||||
this.isHiddenSave = true;
|
this.isHiddenSave = true;
|
||||||
this.isHiddenSpinner = false;
|
this.isHiddenSpinner = false;
|
||||||
await accountsDB.open();
|
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
if (this.numAccounts === 0) {
|
||||||
if (num_accounts === 0) {
|
|
||||||
console.error("Error: there is no account.");
|
console.error("Error: there is no account.");
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
this.SaveProject(identity);
|
this.SaveProject(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,9 +302,5 @@ export default class NewEditProjectView extends Vue {
|
|||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This same popup code is in many files.
|
|
||||||
alertTitle = "";
|
|
||||||
alertMessage = "";
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export default class AccountViewView extends Vue {
|
|||||||
loading = true;
|
loading = true;
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await accountsDB.open();
|
|
||||||
const mnemonic = generateSeed();
|
const mnemonic = generateSeed();
|
||||||
// address is 0x... ETH address, without "did:eth:"
|
// address is 0x... ETH address, without "did:eth:"
|
||||||
const [address, privateHex, publicHex, derivationPath] =
|
const [address, privateHex, publicHex, derivationPath] =
|
||||||
@@ -58,6 +57,8 @@ export default class AccountViewView extends Vue {
|
|||||||
|
|
||||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||||
const identity = JSON.stringify(newId);
|
const identity = JSON.stringify(newId);
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
await accountsDB.accounts.add({
|
await accountsDB.accounts.add({
|
||||||
dateCreated: new Date().toISOString(),
|
dateCreated: new Date().toISOString(),
|
||||||
derivationPath: derivationPath,
|
derivationPath: derivationPath,
|
||||||
|
|||||||
@@ -23,20 +23,31 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
{{ errorMessage }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Project Details -->
|
<!-- Project Details -->
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold">{{ name }}</h2>
|
<div class="block pb-4 flex gap-4">
|
||||||
<div class="flex justify-between gap-4 text-sm mb-3">
|
<div class="flex-none w-16 pt-1">
|
||||||
<span><fa icon="user" class="fa-fw text-slate-400"></fa> Rotary</span>
|
<EntityIcon
|
||||||
<span
|
:entityId="projectId"
|
||||||
><fa icon="calendar" class="fa-fw text-slate-400"></fa
|
:iconSize="64"
|
||||||
>{{ timeSince }}
|
class="block border border-slate-300 rounded-md"
|
||||||
</span>
|
></EntityIcon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<h2 class="text-xl font-semibold">{{ name }}</h2>
|
||||||
|
<div class="text-sm mb-3">
|
||||||
|
<div class="truncate"
|
||||||
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ issuer }}</div
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
><fa icon="calendar" class="fa-fw text-slate-400"></fa
|
||||||
|
>{{ timeSince }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-slate-500">
|
<div class="text-sm text-slate-500">
|
||||||
@@ -65,29 +76,113 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
@click="openDialog({ name: 'you', did: activeDid })"
|
|
||||||
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
|
|
||||||
>
|
|
||||||
I gave...
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>... or choose a contact who gave:</p>
|
<div v-if="activeDid" class="text-center">
|
||||||
<!-- similar contact selection code is in multiple places -->
|
|
||||||
<div class="px-4">
|
|
||||||
<button
|
<button
|
||||||
|
@click="openDialog({ name: 'you', did: activeDid })"
|
||||||
|
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
I gave…
|
||||||
|
</button>
|
||||||
|
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
|
||||||
|
</div>
|
||||||
|
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
|
||||||
|
|
||||||
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
|
<li @click="openDialog()">
|
||||||
|
<div class="mb-1">
|
||||||
|
<fa icon="question-circle" class="fa-fw fa-xl text-slate-400"></fa>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
|
>
|
||||||
|
Anonymous
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
v-for="contact in allContacts"
|
v-for="contact in allContacts"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
@click="openDialog(contact)"
|
@click="openDialog(contact)"
|
||||||
class="text-blue-500"
|
|
||||||
>
|
>
|
||||||
{{ contact.name }},
|
<div class="mb-1">
|
||||||
</button>
|
<fa icon="user" class="fa-fw fa-xl text-slate-400"></fa>
|
||||||
<span v-if="allContacts.length > 0"> or </span>
|
</div>
|
||||||
<button @click="openDialog()" class="text-blue-500">
|
<h3
|
||||||
someone not specified
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||||
</button>
|
>
|
||||||
|
{{ contact.name || "(no name)" }}
|
||||||
|
</h3>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||||
|
<router-link
|
||||||
|
v-if="allContacts.length > 7"
|
||||||
|
:to="{ name: 'contact-gives' }"
|
||||||
|
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||||
|
>
|
||||||
|
Show More Contacts…
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gifts to & from this -->
|
||||||
|
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
|
Given to this Project
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ul class="text-sm border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
v-for="give in givesToThis"
|
||||||
|
:key="give.id"
|
||||||
|
class="py-1.5 border-b border-slate-300"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span
|
||||||
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="give.amount"
|
||||||
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.amount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="give.description" class="text-slate-500">
|
||||||
|
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.description }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
|
…and from this Project
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ul class="text-sm border-t border-slate-300">
|
||||||
|
<li
|
||||||
|
v-for="give in givesByThis"
|
||||||
|
:key="give.id"
|
||||||
|
class="py-1.5 border-b border-slate-300"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between gap-4">
|
||||||
|
<span
|
||||||
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="give.amount"
|
||||||
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.amount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="give.description" class="text-slate-500">
|
||||||
|
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.description }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,45 +192,6 @@
|
|||||||
message="Received from"
|
message="Received from"
|
||||||
>
|
>
|
||||||
</GiftedDialog>
|
</GiftedDialog>
|
||||||
|
|
||||||
<!-- Commit -->
|
|
||||||
<!--
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'new-edit-commitment' }"
|
|
||||||
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
|
|
||||||
>Make Commitment</router-link
|
|
||||||
>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Commitments -->
|
|
||||||
<!--
|
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Commitments</h3>
|
|
||||||
|
|
||||||
<ul class="text-sm border-t border-slate-300">
|
|
||||||
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
|
|
||||||
<span>[Username]</span>
|
|
||||||
<span
|
|
||||||
>5 hours <fa icon="spinner" class="fa-fw text-slate-400"></fa
|
|
||||||
></span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
|
|
||||||
<span>[Username]</span>
|
|
||||||
<span
|
|
||||||
>US$ 20.00 <fa icon="circle-check" class="fa-fw text-lime-500"></fa
|
|
||||||
></span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
|
|
||||||
<span>[Username]</span>
|
|
||||||
<span
|
|
||||||
>0.1 BTC <fa icon="spinner" class="fa-fw text-slate-400"></fa
|
|
||||||
></span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
:alertTitle="alertTitle"
|
:alertTitle="alertTitle"
|
||||||
:alertMessage="alertMessage"
|
:alertMessage="alertMessage"
|
||||||
@@ -146,34 +202,84 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
import * as R from "ramda";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { createAndSubmitGive } from "@/libs/endorserServer";
|
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import {
|
||||||
|
createAndSubmitGive,
|
||||||
|
didInfo,
|
||||||
|
GiveServerRecord,
|
||||||
|
} from "@/libs/endorserServer";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, AlertMessage, QuickNav },
|
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ProjectViewView extends Vue {
|
export default class ProjectViewView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
alertMessage = "";
|
||||||
|
alertTitle = "";
|
||||||
|
allMyDids: Array<string> = [];
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
expanded = false;
|
|
||||||
name = "";
|
|
||||||
description = "";
|
description = "";
|
||||||
|
expanded = false;
|
||||||
|
givesToThis: Array<GiveServerRecord> = [];
|
||||||
|
givesByThis: Array<GiveServerRecord> = [];
|
||||||
|
name = "";
|
||||||
|
issuer = "";
|
||||||
|
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
||||||
|
timeSince = "";
|
||||||
truncatedDesc = "";
|
truncatedDesc = "";
|
||||||
truncateLength = 40;
|
truncateLength = 40;
|
||||||
timeSince = "";
|
|
||||||
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
async created() {
|
||||||
errorMessage = "";
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
|
await accountsDB.open();
|
||||||
|
const accounts = accountsDB.accounts;
|
||||||
|
const accountsArr = await accounts?.toArray();
|
||||||
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||||
|
const account = accountsArr?.find((acc) => acc.did === this.activeDid);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
this.LoadProject(identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load project records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
onEditClick() {
|
onEditClick() {
|
||||||
localStorage.setItem("projectId", this.projectId as string);
|
localStorage.setItem("projectId", this.projectId as string);
|
||||||
@@ -183,6 +289,11 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Isn't there a better way to make this available to the template?
|
||||||
|
didInfo(did, activeDid, dids, contacts) {
|
||||||
|
return didInfo(did, activeDid, dids, contacts);
|
||||||
|
}
|
||||||
|
|
||||||
expandText() {
|
expandText() {
|
||||||
this.expanded = true;
|
this.expanded = true;
|
||||||
}
|
}
|
||||||
@@ -196,11 +307,13 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/claim/byHandle/" +
|
"/api/claim/byHandle/" +
|
||||||
encodeURIComponent(this.projectId);
|
encodeURIComponent(this.projectId);
|
||||||
const token = await accessToken(identity);
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer " + token,
|
|
||||||
};
|
};
|
||||||
|
if (identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
headers["Authorization"] = "Bearer " + token;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
@@ -211,52 +324,125 @@ export default class ProjectViewView extends Vue {
|
|||||||
const now = moment.now();
|
const now = moment.now();
|
||||||
this.timeSince = moment.utc(now).to(eventDate);
|
this.timeSince = moment.utc(now).to(eventDate);
|
||||||
}
|
}
|
||||||
|
this.issuer = resp.data.issuer;
|
||||||
this.name = resp.data.claim?.name || "(no name)";
|
this.name = resp.data.claim?.name || "(no name)";
|
||||||
this.description = resp.data.claim?.description || "(no description)";
|
this.description = resp.data.claim?.description || "(no description)";
|
||||||
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
||||||
} else if (resp.status === 404) {
|
} else if (resp.status === 404) {
|
||||||
// actually, axios throws an error so we never get here
|
// actually, axios throws an error so we never get here
|
||||||
this.errorMessage = "That project does not exist.";
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "That project does not exist.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError.response?.status === 404) {
|
if (serverError.response?.status === 404) {
|
||||||
this.errorMessage = "That project does not exist.";
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "That project does not exist.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.errorMessage =
|
this.$notify(
|
||||||
"Something went wrong retrieving that project." +
|
{
|
||||||
" See logs for more info.";
|
group: "alert",
|
||||||
console.error("Error retrieving project:", error);
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving that project. See logs for more info.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error("Error retrieving project:", serverError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
const givesInUrl =
|
||||||
async created() {
|
this.apiServer +
|
||||||
await db.open();
|
"/api/v2/report/givesForPlans?planIds=" +
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
encodeURIComponent(JSON.stringify([this.projectId]));
|
||||||
this.activeDid = settings?.activeDid || "";
|
try {
|
||||||
this.apiServer = settings?.apiServer || "";
|
const resp = await this.axios.get(givesInUrl, { headers });
|
||||||
this.allContacts = await db.contacts.toArray();
|
if (resp.status === 200 && resp.data.data) {
|
||||||
|
this.givesToThis = resp.data.data;
|
||||||
await accountsDB.open();
|
} else {
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
this.$notify(
|
||||||
if (num_accounts === 0) {
|
{
|
||||||
console.error("Problem! Should have a profile!");
|
group: "alert",
|
||||||
} else {
|
type: "danger",
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
title: "Error",
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
text: "Failed to retrieve gives to this project.",
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
},
|
||||||
if (!identity) {
|
-1,
|
||||||
throw new Error("No identity found.");
|
);
|
||||||
}
|
}
|
||||||
this.LoadProject(identity);
|
} catch (error: unknown) {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving gives to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Error retrieving gives to this project:",
|
||||||
|
serverError.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const givesOutUrl =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/givesProvidedBy?providerId=" +
|
||||||
|
encodeURIComponent(this.projectId);
|
||||||
|
try {
|
||||||
|
const resp = await this.axios.get(givesOutUrl, { headers });
|
||||||
|
if (resp.status === 200 && resp.data.data) {
|
||||||
|
this.givesByThis = resp.data.data;
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to retrieve gives by this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving gives by project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Error retrieving gives by this project:",
|
||||||
|
serverError.message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openDialog(contact) {
|
openDialog(contact) {
|
||||||
this.$refs.customDialog.open(contact);
|
this.$refs.customDialog.open(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDialogResult(result) {
|
handleDialogResult(result) {
|
||||||
if (result.action === "confirm") {
|
if (result.action === "confirm") {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -275,60 +461,84 @@ export default class ProjectViewView extends Vue {
|
|||||||
* @param hours may be 0
|
* @param hours may be 0
|
||||||
*/
|
*/
|
||||||
async recordGive(giverDid, description, hours) {
|
async recordGive(giverDid, description, hours) {
|
||||||
if (this.activeDid == null) {
|
if (!this.activeDid) {
|
||||||
this.alertTitle = "Error";
|
this.$notify(
|
||||||
this.alertMessage =
|
{
|
||||||
"You must select an identity before you can record a give.";
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must select an identity before you can record a give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!description && !hours) {
|
|
||||||
this.alertTitle = "Error";
|
|
||||||
this.alertMessage =
|
|
||||||
"You must enter a description or some number of hours.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
createAndSubmitGive(
|
|
||||||
this.axios,
|
|
||||||
this.apiServer,
|
|
||||||
identity,
|
|
||||||
giverDid,
|
|
||||||
this.activeDid,
|
|
||||||
description,
|
|
||||||
hours,
|
|
||||||
this.projectId
|
|
||||||
)
|
|
||||||
.then((result) => {
|
|
||||||
if (result.status != 201 || result.data?.error) {
|
|
||||||
console.log("Error with give result:", result);
|
|
||||||
this.alertTitle = "Error";
|
|
||||||
this.alertMessage =
|
|
||||||
result.data?.error?.message ||
|
|
||||||
"There was an error recording the give.";
|
|
||||||
} else {
|
|
||||||
this.alertTitle = "Success";
|
|
||||||
this.alertMessage = "That gift was recorded.";
|
|
||||||
//this.updateAllFeed(); // full update is overkill but we should show something
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
// axios throws errors on 400 responses
|
|
||||||
console.log("Error with give caught:", e);
|
|
||||||
this.alertTitle = "Error";
|
|
||||||
this.alertMessage =
|
|
||||||
e.userMessage ||
|
|
||||||
e.response?.data?.error?.message ||
|
|
||||||
"There was an error recording the give.";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This same popup code is in many files.
|
if (!description && !hours) {
|
||||||
alertMessage = "";
|
this.$notify(
|
||||||
alertTitle = "";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You must enter a description or some number of hours.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
const result = await createAndSubmitGive(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
identity,
|
||||||
|
giverDid,
|
||||||
|
this.activeDid,
|
||||||
|
description,
|
||||||
|
hours,
|
||||||
|
this.projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status !== 201 || result.data?.error) {
|
||||||
|
console.log("Error with give result:", result);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
result.data?.error?.message ||
|
||||||
|
"There was an error recording the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Success",
|
||||||
|
text: "That gift was recorded.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Error with give caught:", e);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text:
|
||||||
|
e.userMessage ||
|
||||||
|
e.response?.data?.error?.message ||
|
||||||
|
"There was an error recording the give.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Quick Search -->
|
<!-- Quick Search -->
|
||||||
<form id="QuickSearch" class="mb-4 flex">
|
|
||||||
|
<div id="QuickSearch" class="mb-4 flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search…"
|
placeholder="Search…"
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
>
|
>
|
||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
<!-- New Project -->
|
<!-- New Project -->
|
||||||
<button
|
<button
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<InfiniteScroll @reached-bottom="loadMoreData">
|
<InfiniteScroll @reached-bottom="loadMoreData">
|
||||||
<ul>
|
<ul class="border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300"
|
class="border-b border-slate-300"
|
||||||
v-for="project in projects"
|
v-for="project in projects"
|
||||||
@@ -49,10 +50,11 @@
|
|||||||
class="block py-4 flex gap-4"
|
class="block py-4 flex gap-4"
|
||||||
>
|
>
|
||||||
<div class="flex-none w-12">
|
<div class="flex-none w-12">
|
||||||
<img
|
<EntityIcon
|
||||||
src="https://picsum.photos/200/200?random=1"
|
:entityId="project.handleId"
|
||||||
class="w-full rounded"
|
:iconSize="48"
|
||||||
/>
|
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||||
|
></EntityIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
@@ -73,7 +75,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as R from "ramda";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
@@ -82,31 +83,10 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll";
|
import InfiniteScroll from "@/components/InfiniteScroll";
|
||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
import EntityIcon from "@/components/EntityIcon";
|
||||||
/**
|
|
||||||
* Represents data about a project
|
|
||||||
**/
|
|
||||||
interface ProjectData {
|
|
||||||
/**
|
|
||||||
* Name of the project
|
|
||||||
**/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* Description of the project
|
|
||||||
**/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* URL referencing information about the project
|
|
||||||
**/
|
|
||||||
handleId: string;
|
|
||||||
/**
|
|
||||||
* The Identier of the project
|
|
||||||
**/
|
|
||||||
rowid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { InfiniteScroll, AlertMessage, QuickNav },
|
components: { InfiniteScroll, AlertMessage, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ProjectsView extends Vue {
|
export default class ProjectsView extends Vue {
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
@@ -115,6 +95,12 @@ export default class ProjectsView extends Vue {
|
|||||||
isLoading = false;
|
isLoading = false;
|
||||||
alertTitle = "";
|
alertTitle = "";
|
||||||
alertMessage = "";
|
alertMessage = "";
|
||||||
|
numAccounts = 0;
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core project data loader
|
* Core project data loader
|
||||||
@@ -130,19 +116,27 @@ export default class ProjectsView extends Vue {
|
|||||||
try {
|
try {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200 || !resp.data.data) {
|
||||||
const plans: ProjectData[] = resp.data.data;
|
const plans: ProjectData[] = resp.data.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId = plan.fullIri, rowid } = plan;
|
const { name, description, handleId = plan.fullIri, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, rowid });
|
this.projects.push({ name, description, handleId, rowid });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(resp.status);
|
console.log("Bad server response & data:", resp.status, resp.data);
|
||||||
|
throw Error("Failed to get projects from the server.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Got error loading projects:", error.message);
|
console.error("Got error loading projects:", error.message);
|
||||||
this.alertTitle = "Error";
|
this.$notify(
|
||||||
this.alertMessage = "Got an error loading projects:" + error.message;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Got an error loading projects: " + error.message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -183,6 +177,22 @@ export default class ProjectsView extends Vue {
|
|||||||
await this.dataLoader(url, token);
|
await this.dataLoader(url, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getIdentity(activeDid) {
|
||||||
|
await accountsDB.open();
|
||||||
|
const account = await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first();
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load project records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 'created' hook runs when the Vue instance is first created
|
* 'created' hook runs when the Vue instance is first created
|
||||||
**/
|
**/
|
||||||
@@ -193,26 +203,33 @@ export default class ProjectsView extends Vue {
|
|||||||
const activeDid = settings?.activeDid || "";
|
const activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
|
||||||
await accountsDB.open();
|
if (this.numAccounts === 0) {
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
console.error("No accounts found.");
|
||||||
if (num_accounts === 0) {
|
this.$notify(
|
||||||
console.error("Problem! You need a profile!");
|
{
|
||||||
this.alertTitle = "Error!";
|
group: "alert",
|
||||||
this.alertMessage = "Problem! You need a profile!";
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "You need an identity to load your projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const identity = await this.getIdentity(activeDid);
|
||||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error("No identity found.");
|
|
||||||
}
|
|
||||||
this.current = identity;
|
this.current = identity;
|
||||||
this.LoadProjects(identity);
|
this.LoadProjects(identity);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log("Error initializing:", err);
|
||||||
this.alertTitle = "Error!";
|
this.$notify(
|
||||||
this.alertMessage = "Problem! You need a profile!";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong loading your projects.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,22 +20,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeAccount">
|
<div v-if="activeAccount">
|
||||||
<p>
|
<p class="text-center mb-4">
|
||||||
BEWARE: Anyone who gets hold of this mnemonic seed phrase will be able
|
<b class="text-red-600">BEWARE!</b> Anyone who has this seed phrase will
|
||||||
impersonate you and take over any digital holdings based on it. So only
|
be able impersonate you and take over any digital holdings based on it.
|
||||||
reveal it when you are in a private place out of sight of other eyes,
|
Reveal it when you are somewhere only you can see your screen, and
|
||||||
and only record it in something private -- don't take a screenshot or
|
record it somewhere only you have access.
|
||||||
send it to any online service.
|
<i>Don't take a screenshot or send it to any online service.</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<div class="bg-slate-100 rounded-md overflow-hidden p-4 mb-4">
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
<button
|
||||||
@click="showSeedPhrase"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||||
>
|
@click="showSeedPhrase"
|
||||||
Click here when you're ready to see it.
|
>
|
||||||
</button>
|
Reveal my Seed Phrase
|
||||||
|
</button>
|
||||||
|
|
||||||
<p v-if="showSeed">{{ activeAccount.mnemonic }}</p>
|
<p v-if="showSeed" class="text-center text-slate-700 mt-2">
|
||||||
|
{{ activeAccount.mnemonic }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>You do not have an active identity.</div>
|
<div v-else>You do not have an active identity.</div>
|
||||||
<AlertMessage
|
<AlertMessage
|
||||||
@@ -72,8 +76,15 @@ export default class SeedBackupView extends Vue {
|
|||||||
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Got an error loading an identity:", err);
|
console.error("Got an error loading an identity:", err);
|
||||||
this.alertTitle = "Error Loading Account";
|
this.$notify(
|
||||||
this.alertMessage = "Got an error loading your seed data.";
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Loading Account",
|
||||||
|
text: "Got an error loading your seed data.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class StartView extends Vue {
|
export default class StartView extends Vue {
|
||||||
|
|||||||
@@ -39,15 +39,7 @@
|
|||||||
:alertMessage="alertMessage"
|
:alertMessage="alertMessage"
|
||||||
></AlertMessage>
|
></AlertMessage>
|
||||||
</section>
|
</section>
|
||||||
</template> /**
|
</template>
|
||||||
// from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#examples
|
|
||||||
// Adds a blank image const dataBlob = document
|
|
||||||
.querySelector("#scene-container") .firstChild.toBlob((blob) => { const newImg =
|
|
||||||
document.createElement("img"); const url = URL.createObjectURL(blob);
|
|
||||||
newImg.onload = () => { // no longer need to read the blob so it's revoked
|
|
||||||
URL.revokeObjectURL(url); }; newImg.src = url;
|
|
||||||
document.body.appendChild(newImg); }); **/
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
|
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
@@ -55,11 +47,6 @@ import { World } from "@/components/World/World.js";
|
|||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
|
|
||||||
interface WorldProperties {
|
|
||||||
startTime?: string;
|
|
||||||
endTime?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({ components: { AlertMessage, World, QuickNav } })
|
@Component({ components: { AlertMessage, World, QuickNav } })
|
||||||
export default class StatisticsView extends Vue {
|
export default class StatisticsView extends Vue {
|
||||||
world: World;
|
world: World;
|
||||||
@@ -70,14 +57,20 @@ export default class StatisticsView extends Vue {
|
|||||||
mounted() {
|
mounted() {
|
||||||
try {
|
try {
|
||||||
const container = document.querySelector("#scene-container");
|
const container = document.querySelector("#scene-container");
|
||||||
console.log(container);
|
|
||||||
const newWorld = new World(container, this);
|
const newWorld = new World(container, this);
|
||||||
newWorld.start();
|
newWorld.start();
|
||||||
this.world = newWorld;
|
this.world = newWorld;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
this.alertTitle = "Mounting error";
|
this.$notify(
|
||||||
this.alertMessage = err.message;
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Mounting Error",
|
||||||
|
text: err.message,
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user