Compare commits

...

50 Commits

Author SHA1 Message Date
Jose Olarte III
b6e344a15e Propagated button improvements across views 2024-03-21 19:30:42 +08:00
Jose Olarte III
3d1c46aef8 Merge branch 'master' into button-visual-enhancement 2024-03-21 15:40:54 +08:00
ce05f7d003 Merge pull request 'tweak imagery so that it doesn't get stretched on a mobile device' (#107) from photo-ratio into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#107
2024-03-20 20:26:35 -04:00
313cd79e60 finalize the photo-taking code, adding comments and removing logging 2024-03-20 18:23:57 -06:00
121991b53a Merge branch 'gifted-camera-improvements' into photo-ratio 2024-03-20 16:54:39 -06:00
Jose Olarte III
cbf8cb9f46 Fixed placement of upload/retry buttons 2024-03-20 21:11:01 +08:00
Jose Olarte III
fe0668e4b3 Improved Camera Popup 2024-03-20 19:17:18 +08:00
a230506d96 change the X and picture button so that landscape is all functional (if not great-looking) 2024-03-19 21:02:57 -06:00
c49c55d394 change the photo ratios to fix all but portrait-orientation on mobile-emulation 2024-03-18 22:20:02 -06:00
ae572afff6 add help for when service workers get stuck; bump to version 0.3.2 2024-03-17 20:20:13 -06:00
ccea2486e4 change build for test servers, bump version to 0.3.1 2024-03-17 16:42:49 -06:00
155343a9d7 bump to v 0.3.0 2024-03-17 10:49:28 -06:00
85ad295eb9 Merge pull request 'photo-upload' (#105) from photo-upload into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#105
2024-03-17 12:23:32 -04:00
64322b2804 change the default image server port 2024-03-17 10:22:05 -06:00
3e556dfa52 move the "part of project" text in giving-details screen 2024-03-17 08:53:14 -06:00
252952e017 show the image rate limits 2024-03-15 16:42:25 -06:00
251986d2bc make the photo show in a pop-up dialog 2024-03-14 19:44:11 -06:00
49bb1c07b7 fix file extension 2024-03-14 08:46:28 -06:00
67f34f9826 on "give details" page, distinguish between project & user destination 2024-03-10 17:49:53 -06:00
476d35452a send the claim type with an image 2024-03-10 17:37:10 -06:00
26582030df add another check when deleting an image 2024-03-10 17:36:49 -06:00
ae857f4c8f guard against another set of errors when deleting an image 2024-03-10 16:46:54 -06:00
c602c5ce50 add some other image deletions in other cases 2024-03-10 14:53:41 -06:00
e4543457e2 add image onto give claim, then display on feel (full round-trip, baby!) 2024-03-09 19:37:14 -07:00
c58f012d2c allow viewing and deletion of an image 2024-03-08 23:56:19 -07:00
792e9cb648 separate picture taking from uploading 2024-03-08 09:54:10 -07:00
acee761906 add page for extended details of gifts including pic (not fully tested) 2024-03-08 01:10:17 -07:00
cae2bbc4ff make styled button to take picture 2024-03-07 09:09:51 -07:00
Jose Olarte III
a5c3600673 Sample button visual enhancement 2024-03-07 18:57:08 +08:00
0eb64ed716 add authentication token for image server, change default image server to localhost 2024-03-06 06:12:41 -07:00
f1bb1b51aa Merge branch 'master' into photo-upload 2024-03-05 20:25:41 -07:00
92b924643e fix camera resolution, parameterize image API server 2024-03-05 20:20:54 -07:00
ca90447700 fix different "environment" variables for prod & dev 2024-03-02 16:15:03 -07:00
750700e75e bump version and add '-beta' 2024-03-01 15:59:38 -07:00
3612ea4224 bump to v 0.2.17; add "personalized" message and better confirmation-result messages 2024-03-01 15:54:50 -07:00
dbccbf7e4a fix: show on the confirmation page when there are hidden claims 2024-03-01 14:40:51 -07:00
1258cf02a1 bump to v 0.2.15 2024-03-01 14:06:01 -07:00
a488a36bc0 Merge pull request 'Shortcut page for BVC assertions & confirmations' (#103) from bvc-shortcut into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#103
2024-03-01 15:14:01 -05:00
a93b556e0c doc: refactor tasks 2024-02-26 19:43:23 -07:00
2c28913d97 for BVC: finish submission of confirmations & final give 2024-02-26 19:27:34 -07:00
0b24d7bbd8 for BVC: fix the attendee & show appropriate success message 2024-02-25 18:55:58 -07:00
2058205150 for BVC shortcut: send attend & give actions, and list actions to confirm 2024-02-25 18:38:54 -07:00
866dcb3a2a add screens for the shortcuts for the BVC group (doesn't submit yet) 2024-02-24 18:38:11 -07:00
6aab1ff49d consolidate interface and remove copies of code 2024-02-24 10:26:12 -07:00
c696de33f3 add page to take a picture and upload to an image server 2024-02-23 19:02:10 -07:00
c239db6a4f doc: update tasks 2024-02-19 19:44:59 -07:00
3eda5f6b5d show more succinct info in feed, targeted toward user's visibility 2024-02-19 19:43:55 -07:00
783b38df65 order contacts by name & note outside network as "outside your network" 2024-02-18 14:58:10 -07:00
3475c32e1f update onboarding hint message, justify text on QR page 2024-02-17 12:55:30 -07:00
dcd881adae make the name-setting prompt yellow 2024-02-17 12:46:17 -07:00
47 changed files with 2436 additions and 580 deletions

7
.env.development Normal file
View File

@@ -0,0 +1,7 @@
# I tried setting values here and using `vue-cli-service build --mode development`
# but it didn't create some things in "dist":
# - the "css" directory with the CSS extracted from Vue files
# - the sw_scripts-combined* files
#
# ¯\_(ツ)_/¯

4
.env.production Normal file
View File

@@ -0,0 +1,4 @@
# Only the variables that start with VUE_APP_ are seen in the application process.env in Vue.
VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
VUE_APP_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
VUE_APP_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app

View File

@@ -6,13 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Changed in DB ### Changed in DB or environment
- ? - Nothing
## [0.2.14] - 2024.02.14 ## [0.3.3] - 2024.03.18
### Added
- Photo on gift record
### Fixed
- Environment variable for BVC meetings project
### Changed in DB or environment
- New environment variable for image API server
- Test that a new browser session will get the right default API.
- Test that a new browser session will send the right BVC meetings project.
## [0.2.17] - 2024.03.01 - 3612ea42240c5e1b7d7eff29a39ff18f1b869b36
### Added
- Shortcut page for Bountiful Voluntaryist Community
### Changed ### Changed
- Combine all service worker scripts into a single file - More readable, targeted summaries in home-page feed items
### Changed in DB
- Nothing
## [0.2.14] - 2024.02.14 - 5f9edea1167dbfb64e16648764eed8c09b24eaeb
### Changed
- Combine all service worker scripts into a single file.
### Changed in DB ### Changed in DB
- Nothing - Nothing

View File

@@ -32,21 +32,35 @@ npm run lint
* `npx prettier --write ./sw_scripts/` * `npx prettier --write ./sw_scripts/`
* Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit. * Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`.
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) * Record what version is currently on production.
... though maybe you do that after testing and release, since that isn't used in the build (and you often increment a lot during testing). * Run the correct build
* If production: change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test". Also record what version is on production. * Test
```
# (See .env.development for more details.)
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VUE_APP_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VUE_APP_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build
```
* `npm run build` * Production
```
# This picks up values from .env.production
npm run build
```
* Get on the server and back up the time-safari folder. * Get on the server and back up the DB and the time-safari folder.
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari` * `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
* Revert src/constants/app.ts and package.json (if that was prod), edit package.json to increment version & add "-beta", `npm install`, and commit. Tag if you didn't before. Also record what version is on production. * Revert src/constants/app.ts and package.json (if that was prod).
* Commit changes. Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
* [Tag with the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)

28
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "TimeSafari_Test", "name": "TimeSafari",
"version": "0.2.15-beta", "version": "0.3.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TimeSafari_Test", "name": "TimeSafari",
"version": "0.2.15-beta", "version": "0.3.3",
"dependencies": { "dependencies": {
"@dicebear/collection": "^5.3.5", "@dicebear/collection": "^5.3.5",
"@dicebear/core": "^5.3.5", "@dicebear/core": "^5.3.5",
@@ -17,6 +17,7 @@
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.4.2",
"@veramo/core": "^5.4.1", "@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.4.1", "@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.4.1", "@veramo/data-store": "^5.4.1",
@@ -41,7 +42,7 @@
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"localstorage-slim": "^2.5.0", "localstorage-slim": "^2.5.0",
"luxon": "^3.4.3", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"moment": "^2.29.4", "moment": "^2.29.4",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
@@ -53,6 +54,7 @@
"readable-stream": "^4.4.2", "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",
"simple-vue-camera": "^1.1.3",
"three": "^0.156.1", "three": "^0.156.1",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"util": "^0.12.5", "util": "^0.12.5",
@@ -9170,6 +9172,11 @@
"@types/geojson": "*" "@types/geojson": "*"
} }
}, },
"node_modules/@types/luxon": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="
},
"node_modules/@types/mime": { "node_modules/@types/mime": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
@@ -20251,9 +20258,9 @@
} }
}, },
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.4.3", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -25432,6 +25439,11 @@
"node": ">= 5.10.0" "node": ">= 5.10.0"
} }
}, },
"node_modules/simple-vue-camera": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/simple-vue-camera/-/simple-vue-camera-1.1.3.tgz",
"integrity": "sha512-GVAYq1BMI9cHt+h24tu2dfIFFvhjVQ1M8IkK5LmrKcYoBA8FZlLNlhrHC2NnTPbMAXIvJn1Bqx8X6Q31+Y2+jA=="
},
"node_modules/sirv": { "node_modules/sirv": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "TimeSafari_Test", "name": "TimeSafari",
"version": "0.2.15-beta", "version": "0.3.3",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@@ -17,6 +17,7 @@
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.4.2",
"@veramo/core": "^5.4.1", "@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.4.1", "@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.4.1", "@veramo/data-store": "^5.4.1",
@@ -41,7 +42,7 @@
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"localstorage-slim": "^2.5.0", "localstorage-slim": "^2.5.0",
"luxon": "^3.4.3", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"moment": "^2.29.4", "moment": "^2.29.4",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
@@ -53,6 +54,7 @@
"readable-stream": "^4.4.2", "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",
"simple-vue-camera": "^1.1.3",
"three": "^0.156.1", "three": "^0.156.1",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"util": "^0.12.5", "util": "^0.12.5",

View File

@@ -1,43 +1,51 @@
tasks : tasks :
- 01 release server & client for fix to project-editing - bug - landscape doesn't show full camera
- but - portrait stretches pic
- add to readme - check version, close tabs & restart phone if necessary
- bug maybe - a new give remembers the previous project
- alert & stop if give amount < 0
- add warning that all data (except ID) is public
- onboarding video
- .5 fix timeSafari.org cert renewals - .1 on feed, don't show "to someone anonymous" if it's to a project
- .1 on ideas, put an "x" to close it
- .2 fix give dialog from "more contacts" off home page to allow giving to this user
- .2 fix bottom of project selection map, where the icons are hidden but a tap goes to the icon's page
- .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window)
- .2 don't show a warning on a totally new project when the authorized agent is set
- .2 anchor hash into BTC - .2 anchor hash into BTC
- .1 add step 1 to onboarding hints to "install" - .2 list the "show more" contacts alphabetically
- 01 bookmarks for BVC - .5 make Time Safari a share_target for images
- 08 add image on profile
- ask to detect location & record it in settings
- if personal location is set, show potential local affiliations
- 24 compelling UI for credential presentations - 24 compelling UI for credential presentations
- discover who in my network has activity on a project - discover who in my network has activity on a project
- 24 compelling UI for statistics (eg. World?) - 24 compelling UI for statistics (eg. World?)
- .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window) - 01 in the feed, group by project or contact or topic or time/$ (via BC)
- 01 separate not-on-platform vs totally anonymous; terminology "unidentified"?
- .2 add links between projects - .2 add links between projects
- 32 image on give :
- Show a camera to take a picture
- Scale the image to a reasonable size
- Upload to a public readable place
- check the rate limits
- use CID
- put the image URL in the claim
- Rates - images erased?
- image not associated with JWT ULID since that's assigned later
- 24 make the contact browsing on the front page something that invites more action - 24 make the contact browsing on the front page something that invites more action
- .2 list the "show more" contacts alphabetically
- .5 change server plan & project endpoints to use jwtId as identifier rather than rowid - .5 change server plan & project endpoints to use jwtId as identifier rather than rowid
- 16 edit offers & gives, or revoke allowing re-creation - 16 edit offers & gives, or revoke allowing re-creation
- .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page. - .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page.
- .1 Add help instructions for "Encryption key has changed" error. (It is a problem if localStorage is cleared, but the contacts & settings remain and they have to restore their seeds.) - .1 Add help instructions for "Encryption key has changed" error. (It is a problem if localStorage is cleared, but the contacts & settings remain and they have to restore their seeds.)
- .5 add more detail on TimeSafari.org
- .1 show better error when user with no ID goes to the "My Project" page - .1 show better error when user with no ID goes to the "My Project" page
- 08 add button to front page to prompt for ideas for gratitude : - 01 in front page prompt for ideas for gratitude :
- show previous on "Your" screen - randomize (not show in order)
- checkboxes - randomize vs show in order, show non-person-oriented messages, show only contacts, show only projects - checkboxes - show non-person-oriented messages, show only contacts, show only projects
- .5 add a notice on the front page if their notifications are off
- 08 allow user to add a time when they want their daily notification - 08 allow user to add a time when they want their daily notification
- .5 prompt for the name directly when they visit the QR scan page - .5 prompt for the name directly when they visit the QR scan page
@@ -51,7 +59,7 @@ tasks :
- .1 hide project-create button on project page if not registered - .1 hide project-create button on project page if not registered
- .1 hide offer & give buttons on project list page if not registered - .1 hide offer & give buttons on project list page if not registered
- .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page - .1 add cursor-pointer on the icons for giving on the project page, and on the list of projects on the discover page
- .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads - .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads (feed, project list, give & offer lists)
- bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.) - bug - turning notifications on from the help screen did not stay, though account screen toggle did stay (From Jason on iPhone.)
- refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute) - refactor - supply the projectId to the OfferDialog just like we do with the GiftedDialog offerId (in the "open" method, maybe as well as an attribute)
@@ -74,6 +82,7 @@ tasks :
- create a help-desk document & add screenshots - create a help-desk document & add screenshots
- .1 update "offer" units to have same functionality as "give" units - .1 update "offer" units to have same functionality as "give" units
- .5 add a link to any 'give' records that fulfill an offer on ClaimView
- 01 on home page, prompt for install check in addition to "supports notifications" check (since they won't get notified if Chrome is closed) - 01 on home page, prompt for install check in addition to "supports notifications" check (since they won't get notified if Chrome is closed)
- 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list) - 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list)
- bug (that is hard to reproduce) - got error adding on Firefox user #0 as contact for themselves - bug (that is hard to reproduce) - got error adding on Firefox user #0 as contact for themselves
@@ -114,6 +123,7 @@ tasks :
- .5 show seed phrase in a QR code for transfer to another device - .5 show seed phrase in a QR code for transfer to another device
- .5 on DiscoverView, switch to a filter UI (eg. just from friend - .5 on DiscoverView, switch to a filter UI (eg. just from friend
- .5 don't show "Offer" on project screen if they aren't registered - .5 don't show "Offer" on project screen if they aren't registered
- 01 especially for iOS, check for new version & update, eg. https://stackoverflow.com/questions/52221805/any-way-yet-to-auto-update-or-just-clear-the-cache-on-a-pwa-on-ios
- 24 Move to Vite - 24 Move to Vite
- 32 accept images for projects - 32 accept images for projects

View File

@@ -148,6 +148,37 @@
class="w-full" class="w-full"
role="alert" role="alert"
> >
<div
v-if="notification.type === 'confirm'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<p class="text-lg mb-4">
{{ notification.title }}
</p>
<button
@click="
notification.onYes();
close(notification.id);
"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Yes
</button>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Cancel
</button>
</div>
</div>
</div>
<div <div
v-if="notification.type === 'notification-permission'" v-if="notification.type === 'notification-permission'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
@@ -288,21 +319,14 @@ interface VapidResponse {
}; };
} }
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { sendTestThroughPushServer } from "@/libs/util"; import { sendTestThroughPushServer } from "@/libs/util";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class App extends Vue { export default class App extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
b64 = ""; b64 = "";
serviceWorkerReady = false; serviceWorkerReady = false;
@@ -589,7 +613,7 @@ export default class App extends Vue {
} }
}) })
.catch((error) => { .catch((error) => {
console.log("Push provider server communication failed:", error); console.error("Push provider server communication failed:", error);
return false; return false;
}); });
@@ -604,7 +628,7 @@ export default class App extends Vue {
return response.ok; return response.ok;
}) })
.catch((error) => { .catch((error) => {
console.log("Push server communication failed:", error); console.error("Push server communication failed:", error);
return false; return false;
}); });

View File

@@ -10,23 +10,22 @@
placeholder="What was received" placeholder="What was received"
v-model="description" v-model="description"
/> />
<div class="flex flex-row"> <div class="flex flex-row justify-center">
<span <span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2" class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()" @click="changeUnitCode()"
> >
{{ libsUtil.UNIT_SHORT[unitCode] }} {{ libsUtil.UNIT_SHORT[unitCode] }}
</span> </span>
<div <div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()" @click="amountInput === '0' ? null : decrement()"
v-if="amountInput !== '0'"
> >
<fa icon="chevron-left" /> <fa icon="chevron-left" />
</div> </div>
<input <input
type="number" type="number"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center" class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput" v-model="amountInput"
/> />
<div <div
@@ -36,27 +35,39 @@
<fa icon="chevron-right" /> <fa icon="chevron-right" />
</div> </div>
</div> </div>
<div class="mt-2 text-right"> <div class="mt-4 flex justify-center">
<span v-if="showGivenToUser" class="mr-16">
<input type="checkbox" class="mr-2" v-model="givenToUser" />
<label class="text-sm">Given to you</label>
</span>
<span> <span>
<input type="checkbox" class="mr-2" v-model="isTrade" /> <router-link
<label class="text-sm">Trade (not a gift)</label> :to="{
name: 'gifted-details',
query: {
amountInput,
description,
giverDid: giver?.did,
giverName: giver?.name,
message,
offerId,
projectId,
unitCode,
},
}"
class="text-blue-500"
>
More Options
</router-link>
</span> </span>
</div> </div>
<p class="text-center mb-2 mt-6 italic"> <p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
</p> </p>
<button <button
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@click="confirm" @click="confirm"
> >
Sign &amp; Send Sign &amp; Send
</button> </button>
<button <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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel" @click="cancel"
> >
Cancel Cancel
@@ -67,6 +78,8 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
@@ -75,19 +88,11 @@ import {
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop message = ""; @Prop message = "";
@Prop projectId = ""; @Prop projectId = "";
@@ -99,9 +104,9 @@ export default class GiftedDialog extends Vue {
apiServer = ""; apiServer = "";
amountInput = "0"; amountInput = "0";
giver?: GiverInputInfo; // undefined means no identified giver agent
description = ""; description = "";
givenToUser = false; givenToUser = false;
giver?: GiverInputInfo; // undefined means no identified giver agent
isTrade = false; isTrade = false;
offerId = ""; offerId = "";
unitCode = "HUR"; unitCode = "HUR";
@@ -211,22 +216,6 @@ export default class GiftedDialog extends Vue {
}); });
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records for DID ${activeDid} but no identifier was found",
);
}
return identity;
}
/** /**
* *
* @param giverDid may be null * @param giverDid may be null
@@ -267,7 +256,7 @@ export default class GiftedDialog extends Vue {
} }
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,

View File

@@ -0,0 +1,314 @@
<template>
<div v-if="visible" class="dialog-overlay z-[60]">
<div class="dialog relative">
<div class="text-lg text-center font-light relative z-50">
<div
id="ViewHeading"
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-2 bg-black/50 text-white leading-none"
>
<span v-if="uploading"> Uploading... </span>
<span v-else-if="blob"> Look Good? </span>
<span v-else> Say "Cheese"! </span>
</div>
<div
class="text-lg text-center p-2 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</div>
<div v-if="uploading" class="flex justify-center">
<fa icon="spinner" class="fa-spin fa-3x text-center block" />
</div>
<div v-else-if="blob">
<div
class="flex justify-center gap-2 absolute bottom-[1rem] left-[1rem] right-[1rem] bg-black/50 px-4 py-2"
>
<button
@click="uploadImage"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white font-bold py-2 px-4 rounded-md"
>
<span>Upload</span>
</button>
<button
@click="retryImage"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white font-bold py-2 px-4 rounded-md"
>
<span>Retry</span>
</button>
</div>
<div class="flex justify-center">
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" />
</div>
</div>
<div v-else>
<!--
Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically:
:resolution="{ width: 375, height: 812 }"
-->
<camera facingMode="environment" autoplay ref="camera">
<div
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 landscape:right-0 landscape:top-0 landscape:bottom-0 flex landscape:flex-row justify-center items-center portrait:pb-2 landscape:pr-4"
>
<button
@click="takeImage"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="camera" class="w-[1em]"></fa>
</button>
</div>
</camera>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from "axios";
import Camera from "simple-vue-camera";
import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { getIdentity } from "@/libs/util";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
@Component({ components: { Camera } })
export default class GiftedPhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
blob: Blob | null = null;
setImage: (arg: string) => void = () => {};
imageHeight?: number = window.innerHeight / 2;
imageWidth?: number = window.innerWidth / 2;
imageWarning = ".";
uploading = false;
visible = false;
URL = window.URL || window.webkitURL;
async mounted() {
try {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your settings.",
},
-1,
);
}
}
open(setImageFn: (arg: string) => void) {
this.visible = true;
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) {
bottomNav.style.display = "none";
}
this.setImage = setImageFn;
}
close() {
this.visible = false;
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) {
bottomNav.style.display = "";
}
this.blob = null;
}
async takeImage(/* payload: MouseEvent */) {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
/**
* This logic to set the image height & width correctly.
* Without it, the portrait orientation ends up with an image that is stretched horizontally.
* Note that it's the same with raw browser Javascript; see the "drawImage" example below.
* Now that I've done it, I can't explain why it works.
*/
let imageHeight = cameraComponent?.resolution?.height;
let imageWidth = cameraComponent?.resolution?.width;
const initialImageRatio = imageWidth / imageHeight;
const windowRatio = window.innerWidth / window.innerHeight;
if (initialImageRatio > 1 && windowRatio < 1) {
// the image is wider than it is tall, and the window is taller than it is wide
// For some reason, mobile in portrait orientation renders a horizontally-stretched image.
// We're gonna force it opposite.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
} else if (initialImageRatio < 1 && windowRatio > 1) {
// the image is taller than it is wide, and the window is wider than it is tall
// Haven't seen this happen, but we'll do it just in case.
imageHeight = cameraComponent?.resolution?.width;
imageWidth = cameraComponent?.resolution?.height;
}
const newImageRatio = imageWidth / imageHeight;
if (newImageRatio < windowRatio) {
// the image is a taller ratio than the window, so fit the height first
imageHeight = window.innerHeight / 2;
imageWidth = imageHeight * newImageRatio;
} else {
// the image is a wider ratio than the window, so fit the width first
imageWidth = window.innerWidth / 2;
imageHeight = imageWidth / newImageRatio;
}
// The resolution is only necessary because of that mobile portrait-orientation case.
// The mobile emulation in a browser shows something stretched vertically, but real devices work fine.
this.blob = await cameraComponent?.snapshot({
height: imageHeight,
width: imageWidth,
}); // png is default; if that changes, change extension in formData.append
if (!this.blob) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error taking the picture. Please try again.",
},
5000,
);
return;
}
}
async retryImage() {
this.blob = null;
}
/****
Here's an approach to photo capture without a library. It has similar quirks.
Now that we've fixed styling for simple-vue-camera, it's not critical to refactor. Maybe someday.
<button id="start-camera" @click="cameraClicked">Start Camera</button>
<video id="video" width="320" height="240" autoplay></video>
<button id="snap-photo" @click="photoSnapped">Snap Photo</button>
<canvas id="canvas" width="320" height="240"></canvas>
async cameraClicked() {
console.log("camera_button clicked");
const video = document.querySelector("#video");
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
if (video instanceof HTMLVideoElement) {
video.srcObject = stream;
}
}
photoSnapped() {
console.log("snap_photo clicked");
const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas");
if (
canvas instanceof HTMLCanvasElement &&
video instanceof HTMLVideoElement
) {
canvas
?.getContext("2d")
?.drawImage(video, 0, 0, canvas.width, canvas.height);
// ... or set the blob:
// canvas?.toBlob(
// (blob) => {
// this.blob = blob;
// },
// "image/jpeg",
// 1,
// );
// data url of the image
const image_data_url = canvas?.toDataURL("image/jpeg");
console.log(image_data_url);
}
}
****/
async uploadImage() {
this.uploading = true;
const identifier = await getIdentity(this.activeDid);
const token = await accessToken(identifier);
const headers = {
Authorization: "Bearer " + token,
};
const formData = new FormData();
if (!this.blob) {
// yeah, this should never happen, but it helps with subsequent type checking
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error finding the picture. Please try again.",
},
5000,
);
this.uploading = false;
return;
}
formData.append("image", this.blob, "snapshot.png"); // png is set in snapshot()
formData.append("claimType", "GiveAction");
try {
const response = await axios.post(
DEFAULT_IMAGE_API_SERVER + "/image",
formData,
{ headers },
);
this.uploading = false;
this.visible = false;
this.blob = null;
this.setImage(response.data.url as string);
} catch (error) {
console.error("Error uploading the image", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error saving the picture. Please try again.",
},
5000,
);
this.uploading = false;
this.blob = null;
}
}
}
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 700px;
}
</style>

View File

@@ -33,7 +33,7 @@
<span class="flex justify-between"> <span class="flex justify-between">
<span /> <span />
<button <button
class="text-center bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4" class="text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4"
@click="nextIdeaPastContacts()" @click="nextIdeaPastContacts()"
> >
Skip Contacts <fa icon="forward" /> Skip Contacts <fa icon="forward" />
@@ -52,7 +52,7 @@
</span> </span>
</span> </span>
<button <button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4"
@click="cancel" @click="cancel"
> >
That's it! That's it!
@@ -64,20 +64,13 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import { AppString } from "@/constants/app"; import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class GivenPrompts extends Vue { export default class GivenPrompts extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
IDEAS = [ IDEAS = [
"Did anyone fix food for you?", "Did anyone fix food for you?",

View File

@@ -51,13 +51,13 @@
Sign & Send to publish to the world Sign & Send to publish to the world
</p> </p>
<button <button
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@click="confirm" @click="confirm"
> >
Sign &amp; Send Sign &amp; Send
</button> </button>
<button <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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel" @click="cancel"
> >
Cancel Cancel
@@ -68,22 +68,16 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { createAndSubmitOffer } from "@/libs/endorserServer"; import { createAndSubmitOffer } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accountsDB, db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class OfferDialog extends Vue { export default class OfferDialog extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop message = ""; @Prop message = "";
@Prop projectId = ""; @Prop projectId = "";
@@ -107,7 +101,7 @@ export default class OfferDialog extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings from database:", err); console.error("Error retrieving settings from database:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -178,22 +172,6 @@ export default class OfferDialog extends Vue {
}); });
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
`Attempted to load Offer records for DID ${activeDid} but no identifier was found`,
);
}
return identity;
}
/** /**
* *
* @param description may be an empty string * @param description may be an empty string
@@ -233,7 +211,7 @@ export default class OfferDialog extends Vue {
} }
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitOffer( const result = await createAndSubmitOffer(
this.axios, this.axios,
this.apiServer, this.apiServer,
@@ -250,7 +228,7 @@ export default class OfferDialog extends Vue {
this.isOfferCreationError(result.response) this.isOfferCreationError(result.response)
) { ) {
const errorMessage = this.getOfferCreationErrorMessage(result); const errorMessage = this.getOfferCreationErrorMessage(result);
console.log("Error with offer creation result:", result); console.error("Error with offer creation result:", result);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -273,7 +251,7 @@ export default class OfferDialog extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
console.log("Error with offer recordation caught:", error); console.error("Error with offer recordation caught:", error);
const message = const message =
error.userMessage || error.userMessage ||
error.response?.data?.error?.message || error.response?.data?.error?.message ||

View File

@@ -4,20 +4,14 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator"; import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { AppString } from "@/constants/app";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component @Component
export default class TopMessage extends Vue { export default class TopMessage extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop selected = ""; @Prop selected = "";

View File

@@ -12,10 +12,20 @@ export enum AppString {
TEST1_PUSH_SERVER = "https://test.timesafari.app", TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com", TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
PROD_IMAGE_API_SERVER = "https://image-api.timesafari.app",
TEST_IMAGE_API_SERVER = "https://test-image-api.timesafari.app",
LOCAL_IMAGE_API_SERVER = "http://localhost:3001",
NO_CONTACT_NAME = "(no name)", NO_CONTACT_NAME = "(no name)",
} }
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER; export const DEFAULT_ENDORSER_API_SERVER =
process.env.VUE_APP_DEFAULT_ENDORSER_API_SERVER ||
AppString.TEST_ENDORSER_API_SERVER;
export const DEFAULT_IMAGE_API_SERVER =
process.env.VUE_APP_DEFAULT_IMAGE_API_SERVER ||
AppString.TEST_IMAGE_API_SERVER;
export const DEFAULT_PUSH_SERVER = export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host; window.location.protocol + "//" + window.location.host;
@@ -29,4 +39,5 @@ export interface NotificationIface {
type: string; // "toast" | "info" | "success" | "warning" | "danger" type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string; title: string;
text: string; text: string;
onYes?: () => Promise<void>;
} }

View File

@@ -31,6 +31,7 @@ export type Settings = {
}>; }>;
showContactGivesInline?: boolean; // Display contact inline or not showContactGivesInline?: boolean; // Display contact inline or not
showShortcutBvc?: boolean; // Show shortcut for BVC actions
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server warnIfTestServer?: boolean; // Warn if using a testing server

View File

@@ -22,7 +22,7 @@ export interface AgreeVerifiableCredential {
"@type": string; "@type": string;
// "any" because arbitrary objects can be subject of agreement // "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
object: Record<any, any>; object: Record<string, any>;
} }
export interface GiverInputInfo { export interface GiverInputInfo {
@@ -46,23 +46,28 @@ export interface ClaimResult {
export interface GenericVerifiableCredential { export interface GenericVerifiableCredential {
"@context": string; "@context": string;
"@type": string; "@type": string;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
} }
export interface GenericServerRecord extends GenericVerifiableCredential { export interface GenericServerRecord extends GenericVerifiableCredential {
handleId?: string; handleId?: string;
id?: string; id: string;
issuedAt?: string; issuedAt: string;
issuer?: string; issuer: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>; claim: Record<string, any>;
claimType?: string; claimType?: string;
} }
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = { export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
"@type": "", "@type": "",
claim: {}, claim: {},
id: "",
issuedAt: "",
issuer: "",
}; };
// a summary record; the VC is found the fullClaim field
export interface GiveServerRecord { export interface GiveServerRecord {
agentDid: string; agentDid: string;
amount: number; amount: number;
@@ -77,6 +82,7 @@ export interface GiveServerRecord {
unit: string; unit: string;
} }
// a summary record; the VC is found the fullClaim field
export interface OfferServerRecord { export interface OfferServerRecord {
amount: number; amount: number;
amountGiven: number; amountGiven: number;
@@ -94,13 +100,14 @@ export interface OfferServerRecord {
validThrough: string; validThrough: string;
} }
// a summary record; the VC is not currently part of this record
export interface PlanServerRecord { export interface PlanServerRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well agentDid?: string; // optional, if the issuer wants someone else to manage as well
description: string; description: string;
endTime?: string; endTime?: string;
fulfillsPlanHandleId: string; fulfillsPlanHandleId: string;
issuerDid: string;
handleId: string; handleId: string;
issuerDid: string;
locLat?: number; locLat?: number;
locLon?: number; locLon?: number;
startTime?: string; startTime?: string;
@@ -116,6 +123,7 @@ export interface GiveVerifiableCredential {
description?: string; description?: string;
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[]; fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
identifier?: string; identifier?: string;
image?: string;
object?: { amountOfThisGood: number; unitCode: string }; object?: { amountOfThisGood: number; unitCode: string };
recipient?: { identifier: string }; recipient?: { identifier: string };
} }
@@ -179,7 +187,7 @@ export interface PlanData {
rowid?: string; rowid?: string;
} }
export interface RateLimits { export interface EndorserRateLimits {
doneClaimsThisWeek: string; doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string; doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string; maxClaimsPerWeek: string;
@@ -188,6 +196,12 @@ export interface RateLimits {
nextWeekBeginDateTime: string; nextWeekBeginDateTime: string;
} }
export interface ImageRateLimits {
doneImagesThisWeek: string;
maxImagesPerWeek: string;
nextWeekBeginDateTime: string;
}
export interface VerifiableCredential { export interface VerifiableCredential {
"@context": string; "@context": string;
"@type": string; "@type": string;
@@ -226,16 +240,16 @@ export interface ErrorResponse {
}; };
} }
export interface ErrorResult {
type: "error";
error: InternalError;
}
export interface InternalError { export interface InternalError {
error: string; // for system logging error: string; // for system logging
userMessage?: string; // for user display userMessage?: string; // for user display
} }
export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult; export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
// This is used to check for hidden info. // This is used to check for hidden info.
@@ -327,7 +341,7 @@ export function addLastClaimOrHandleAsIdIfMissing(
} }
// return clone of object without any nested *VisibleToDids keys // return clone of object without any nested *VisibleToDids keys
// similar logic is found in endorser-mobile // similar code is also contained in endorser-mobile
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function removeVisibleToDids(input: any): any { export function removeVisibleToDids(input: any): any {
if (input instanceof Object) { if (input instanceof Object) {
@@ -337,7 +351,6 @@ export function removeVisibleToDids(input: any): any {
const result: Record<string, any> = {}; const result: Record<string, any> = {};
for (const key in input) { for (const key in input) {
if (!key.endsWith("VisibleToDids")) { if (!key.endsWith("VisibleToDids")) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result[key] = removeVisibleToDids(R.clone(input[key])); result[key] = removeVisibleToDids(R.clone(input[key]));
} }
} }
@@ -346,16 +359,59 @@ export function removeVisibleToDids(input: any): any {
// it's an array // it's an array
return R.map(removeVisibleToDids, input); return R.map(removeVisibleToDids, input);
} }
return false;
} else { } else {
return input; return input;
} }
} }
/** export function contactForDid(
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY did: string | undefined,
contacts: Contact[],
): Contact | undefined {
return isEmptyOrHiddenDid(did)
? undefined
: R.find((c) => c.did === did, contacts);
}
Similar logic is found in endorser-mobile. /**
*
* Similar logic is found in endorser-mobile.
*
* @param did
* @param activeDid
* @param contact
* @param allMyDids
* @return { known: boolean, displayName: string } where known is true if the display name is some easily-recogizable name, false if it's a generic name like "Someone Anonymous"
*/
export function didInfoForContact(
did: string | undefined,
activeDid: string | undefined,
contact?: Contact,
allMyDids: string[] = [],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): { known: boolean; displayName: string } {
if (!did) return { displayName: "Someone Anonymous", known: false };
if (contact) {
return {
displayName: contact.name || "Contact With No Name",
known: !!contact.name,
};
} else if (did === activeDid) {
return { displayName: "You", known: true };
} else {
const myId = R.find(R.equals(did), allMyDids);
return myId
? { displayName: "You (Alt ID)", known: true }
: isHiddenDid(did)
? { displayName: "Someone Outside Your Network", known: false }
: { displayName: "Someone Outside Contacts", known: false };
}
}
/**
always returns text, maybe something like "unnamed" or "unknown"
Now that we're using more informational didInfoForContact under the covers, we might want to consolidate.
**/ **/
export function didInfo( export function didInfo(
did: string | undefined, did: string | undefined,
@@ -363,19 +419,8 @@ export function didInfo(
allMyDids: string[], allMyDids: string[],
contacts: Contact[], contacts: Contact[],
): string { ): string {
if (!did) return "Someone Anonymous"; const contact = contactForDid(did, contacts);
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
const contact = R.find((c) => c.did === did, contacts);
if (contact) {
return contact.name || "Contact With No Name";
} else {
const myId = R.find(R.equals(did), allMyDids);
return myId
? `You${myId !== activeDid ? " (Alt ID)" : ""}`
: isHiddenDid(did)
? "Someone Not In Network"
: "Someone Not In Contacts";
}
} }
/** /**
@@ -399,6 +444,7 @@ export async function createAndSubmitGive(
fulfillsProjectHandleId?: string, fulfillsProjectHandleId?: string,
fulfillsOfferHandleId?: string, fulfillsOfferHandleId?: string,
isTrade: boolean = false, isTrade: boolean = false,
imageUrl?: string,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
@@ -425,6 +471,9 @@ export async function createAndSubmitGive(
identifier: fulfillsOfferHandleId, identifier: fulfillsOfferHandleId,
}); });
} }
if (imageUrl) {
vcClaim.image = imageUrl;
}
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericServerRecord, vcClaim as GenericServerRecord,
identity, identity,
@@ -482,6 +531,28 @@ export async function createAndSubmitOffer(
); );
} }
// similar logic is found in endorser-mobile
export const createAndSubmitConfirmation = async (
identifier: IIdentifier,
claim: GenericVerifiableCredential,
lastClaimId: string, // used to set the lastClaimId
handleId: string | undefined,
apiServer: string,
axios: Axios,
) => {
const goodClaim = removeSchemaContext(
removeVisibleToDids(
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
),
);
const confirmationClaim: GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
return createAndSubmitClaim(confirmationClaim, identifier, apiServer, axios);
};
export async function createAndSubmitClaim( export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential, vcClaim: GenericVerifiableCredential,
identity: IIdentifier, identity: IIdentifier,
@@ -546,12 +617,199 @@ export async function createAndSubmitClaim(
} }
} }
// from https://stackoverflow.com/a/175787/845494 // eslint-disable-next-line @typescript-eslint/no-explicit-any
// export const isAccept = (claim: Record<string, any>) => {
export function isNumeric(str: string): boolean { return (
return !isNaN(+str); claim &&
claim["@context"] === SCHEMA_ORG_CONTEXT &&
claim["@type"] === "AcceptAction"
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isOffer = (claim: Record<string, any>) => {
return (
claim &&
claim["@context"] === SCHEMA_ORG_CONTEXT &&
claim["@type"] === "Offer"
);
};
export function currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
export function numberOrZero(str: string): number { export function displayAmount(code: string, amt: number) {
return isNumeric(str) ? +str : 0; return "" + amt + " " + currencyShortWordForCode(code, amt === 1);
} }
// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
};
/**
return readable summary of claim, or something generic
similar code is also contained in endorser-mobile
**/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claimSummary = (claim: Record<string, any>) => {
if (!claim) {
// to differentiate from "something" above
return "something";
}
if (claim.claim) {
// probably a Verified Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any
claim = claim.claim as Record<string, any>;
}
if (Array.isArray(claim)) {
if (claim.length === 1) {
claim = claim[0];
} else {
return "multiple claims";
}
}
const type = claim["@type"];
if (!type) {
return "a claim";
} else {
let typeExpl = capitalizeAndInsertSpacesBeforeCaps(type);
if (typeExpl === "Person") {
typeExpl += " claim";
}
return "a " + typeExpl;
}
};
/**
return readable description of claim if possible, as a past-tense action
identifiers is a list of objects with a 'did' field, each representing the user
contacts is a list of objects with a 'did' field for others and a 'name' field for their name
similar code is also contained in endorser-mobile
**/
export const claimSpecialDescription = (
record: GenericServerRecord,
activeDid: string,
identifiers: Array<string>,
contacts: Array<Contact>,
) => {
let claim = record.claim;
if (claim.claim) {
// it's probably a Verified Credential
claim = claim.claim;
}
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
const type = claim["@type"] || "UnknownType";
if (type === "AgreeAction") {
return issuer + " agreed with " + claimSummary(claim.object);
} else if (isAccept(claim)) {
return issuer + " accepted " + claimSummary(claim.object);
} else if (type === "GiveAction") {
// agent.did is for legacy data, before March 2023
const giver = claim.agent?.identifier || claim.agent?.did;
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
let gaveAmount = claim.object?.amountOfThisGood
? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: "";
if (claim.description) {
if (gaveAmount) {
gaveAmount = gaveAmount + ", and also: ";
}
gaveAmount = gaveAmount + claim.description;
}
if (!gaveAmount) {
gaveAmount = "something not described";
}
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientInfo = gaveRecipientId
? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
: "";
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
} else if (type === "JoinAction") {
// agent.did is for legacy data, before March 2023
const agent = claim.agent?.identifier || claim.agent?.did;
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
let eventOrganizer =
claim.event && claim.event.organizer && claim.event.organizer.name;
eventOrganizer = eventOrganizer || "";
let eventName = claim.event && claim.event.name;
eventName = eventName ? " " + eventName : "";
let fullEvent = eventOrganizer + eventName;
fullEvent = fullEvent ? " attended the " + fullEvent : "";
let eventDate = claim.event && claim.event.startTime;
eventDate = eventDate ? " at " + eventDate : "";
return contactInfo + fullEvent + eventDate;
} else if (isOffer(claim)) {
const offerer = claim.offeredBy?.identifier;
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
let offering = "";
if (claim.includesObject) {
offering +=
" " +
displayAmount(
claim.includesObject.unitCode,
claim.includesObject.amountOfThisGood,
);
}
if (claim.itemOffered?.description) {
offering += ", saying: " + claim.itemOffered?.description;
}
// recipient.did is for legacy data, before March 2023
const offerRecipientId =
claim.recipient?.identifier || claim.recipient?.did;
const offerRecipientInfo = offerRecipientId
? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts)
: "";
return contactInfo + " offered" + offering + offerRecipientInfo;
} else if (type === "PlanAction") {
const claimer = claim.agent?.identifier || record.issuer;
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
return claimerInfo + " announced a project: " + claim.name;
} else if (type === "Tenure") {
// party.did is for legacy data, before March 2023
const claimer = claim.party?.identifier || claim.party?.did;
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
const polygon = claim.spatialUnit?.geo?.polygon || "";
return (
contactInfo +
" possesses [" +
polygon.substring(0, polygon.indexOf(" ")) +
"...]"
);
} else {
return issuer + " declared " + claimSummary(claim as GenericServerRecord);
}
};
export const BVC_MEETUPS_PROJECT_CLAIM_ID =
process.env.VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID ||
"https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK"; // this won't resolve as a URL on production; it's a URN only found in the test system
export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
return {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "JoinAction",
agent: {
identifier: did,
},
event: {
organizer: {
name: "Bountiful Voluntaryist Community",
},
name: "Saturday Morning Meeting",
startTime: startTime,
},
};
};

View File

@@ -1,14 +1,16 @@
// many of these are also found in endorser-mobile utility.ts // many of these are also found in endorser-mobile utility.ts
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import { IIdentifier } from "@veramo/core";
import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericServerRecord, containsHiddenDid } from "@/libs/endorserServer"; import { GenericServerRecord, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { useClipboard } from "@vueuse/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;
@@ -16,7 +18,7 @@ const Buffer = require("buffer/").Buffer;
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, too), // If you edit this, check that the numbers still line up on the side in the alert (on mobile, too),
// and make sure they can take all actions while the notification shows. // and make sure they can take all actions while the notification shows.
export const ONBOARD_MESSAGE = export const ONBOARD_MESSAGE =
"1) Check that they have entered their name on the profile page in their device. 2) Add them to your Contacts by scanning with the QR icon that is by the input box. 3) Click the person icon to register them. 4) Have them go to their Contact page and scan your QR to add you to their list."; "1) Read through all their yellow prompts. 2) Add them to your Contacts by scanning with the QR icon that is by the input box. 3) Click the person icon to register them. 4) Show them your QR so they'll scan you. 5) Have them enable notifications.";
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
export const UNIT_SHORT: Record<string, string> = { export const UNIT_SHORT: Record<string, string> = {
@@ -55,6 +57,16 @@ export function iconForUnitCode(unitCode: string) {
return UNIT_CODES[unitCode]?.faIcon || "question"; return UNIT_CODES[unitCode]?.faIcon || "question";
} }
// from https://stackoverflow.com/a/175787/845494
//
export function isNumeric(str: string): boolean {
return !isNaN(+str);
}
export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}
export const isGlobalUri = (uri: string) => { export const isGlobalUri = (uri: string) => {
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
}; };
@@ -180,6 +192,22 @@ export function findAllVisibleToDids(
* *
**/ **/
export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
`Attempted to load Offer records for DID ${activeDid} but no identifier was found`,
);
}
return identity;
};
/** /**
* Generates a new identity, saves it to the database, and sets it as the active identity. * Generates a new identity, saves it to the database, and sets it as the active identity.
* @return {Promise<string>} with the DID of the new identity * @return {Promise<string>} with the DID of the new identity

View File

@@ -18,6 +18,8 @@ import {
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,
faCalendar, faCalendar,
faCamera,
faCheck,
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faCircle, faCircle,
@@ -76,6 +78,8 @@ library.add(
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,
faCalendar, faCalendar,
faCamera,
faCheck,
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faCircle, faCircle,
@@ -127,9 +131,11 @@ library.add(
); );
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Camera from "simple-vue-camera";
createApp(App) createApp(App)
.component("fa", FontAwesomeIcon) .component("fa", FontAwesomeIcon)
.component("camera", Camera)
.use(createPinia()) .use(createPinia())
.use(VueAxios, axios) .use(VueAxios, axios)
.use(router) .use(router)

View File

@@ -28,12 +28,6 @@ const enterOrStart = async (
}; };
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: () =>
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
},
{ {
path: "/account", path: "/account",
name: "account", name: "account",
@@ -90,12 +84,26 @@ const routes: Array<RouteRecordRaw> = [
component: () => component: () =>
import(/* webpackChunkName: "discover" */ "../views/DiscoverView.vue"), import(/* webpackChunkName: "discover" */ "../views/DiscoverView.vue"),
}, },
{
path: "/gifted-details",
name: "gifted-details",
component: () =>
import(
/* webpackChunkName: "gifted-details" */ "../views/GiftedDetails.vue"
),
},
{ {
path: "/help", path: "/help",
name: "help", name: "help",
component: () => component: () =>
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"), import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
}, },
{
path: "/",
name: "home",
component: () =>
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
},
{ {
path: "/help-notifications", path: "/help-notifications",
name: "help-notifications", name: "help-notifications",
@@ -165,6 +173,30 @@ const routes: Array<RouteRecordRaw> = [
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"), import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart, beforeEnter: enterOrStart,
}, },
{
path: "/quick-action-bvc",
name: "quick-action-bvc",
component: () =>
import(
/* webpackChunkName: "quick-action-bvc" */ "../views/QuickActionBvcView.vue"
),
},
{
path: "/quick-action-bvc-begin",
name: "quick-action-bvc-begin",
component: () =>
import(
/* webpackChunkName: "quick-action-bvc-begin" */ "../views/QuickActionBvcBeginView.vue"
),
},
{
path: "/quick-action-bvc-end",
name: "quick-action-bvc-end",
component: () =>
import(
/* webpackChunkName: "quick-action-bvc-end" */ "../views/QuickActionBvcEndView.vue"
),
},
{ {
path: "/scan-contact", path: "/scan-contact",
name: "scan-contact", name: "scan-contact",

View File

@@ -1,5 +1,5 @@
<template> <template>
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile" />
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
@@ -37,7 +37,7 @@
<span> <span>
<router-link <router-link
:to="{ name: 'help' }" :to="{ name: 'help' }"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1" class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
> >
Help Help
</router-link> </router-link>
@@ -55,7 +55,7 @@
</p> </p>
<router-link <router-link
:to="{ name: 'start' }" :to="{ name: 'start' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
> >
Create An Identifier Create An Identifier
</router-link> </router-link>
@@ -96,7 +96,7 @@
<!-- Registration notice --> <!-- Registration notice -->
<!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. --> <!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
<div <div
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime" v-if="!loadingLimits && !endorserLimits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4" class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
> >
<p class="mb-4"> <p class="mb-4">
@@ -105,7 +105,7 @@
</p> </p>
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
> >
Share Your Info Share Your Info
</router-link> </router-link>
@@ -157,31 +157,42 @@
<div> <div>
{{ limitsMessage }} {{ limitsMessage }}
</div> </div>
<div v-if="!!limits?.nextWeekBeginDateTime"> <div v-if="!!endorserLimits?.nextWeekBeginDateTime">
<p class="mb-3 text-sm">
You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of
<b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims
counter resets at
<b class="whitespace-nowrap">{{
readableTime(limits.nextWeekBeginDateTime)
}}</b>
</p>
<p class="text-sm"> <p class="text-sm">
You have done You have done
<b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of <b>{{ endorserLimits.doneClaimsThisWeek }} claims</b> out of
<b>{{ limits.maxRegistrationsPerMonth }}</b> for this month. <b>{{ endorserLimits.maxClaimsPerWeek }}</b> for this week. Your
claims counter resets at
<b class="whitespace-nowrap">{{
readableDate(endorserLimits.nextWeekBeginDateTime)
}}</b>
</p>
<p class="mt-3 text-sm">
You have done
<b>{{ endorserLimits.doneRegistrationsThisMonth }} registrations</b>
out of <b>{{ endorserLimits.maxRegistrationsPerMonth }}</b> for this
month.
<i <i
>(You can register nobody on your first day, and after that only one >(You can register nobody on your first day, and after that only one
a day in your first month.)</i a day in your first month.)</i
> >
Your registration counter resets at Your registration counter resets at
<b class="whitespace-nowrap"> <b class="whitespace-nowrap">
{{ readableTime(limits.nextMonthBeginDateTime) }} {{ readableDate(endorserLimits.nextMonthBeginDateTime) }}
</b> </b>
</p> </p>
<p class="mt-3 text-sm" v-if="!!imageLimits">
You have uploaded
<b>{{ imageLimits?.doneImagesThisWeek }} images</b> out of
<b>{{ imageLimits?.maxImagesPerWeek }}</b> for this week. Your image
counter resets at
<b class="whitespace-nowrap">{{
readableDate(imageLimits?.nextWeekBeginDateTime)
}}</b>
</p>
</div> </div>
<button <button
class="block float-right w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2" class="block float-right w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mt-2"
@click="checkLimits()" @click="checkLimits()"
> >
Recheck Limits Recheck Limits
@@ -193,14 +204,14 @@
<router-link <router-link
:to="{ name: 'seed-backup' }" :to="{ name: 'seed-backup' }"
v-if="activeDid" v-if="activeDid"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2 mt-2" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
> >
Backup Identifier Seed Backup Identifier Seed
</router-link> </router-link>
<button <button
v-bind:class="computedStartDownloadLinkClassNames()" v-bind:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="exportDatabase()" @click="exportDatabase()"
> >
Download Settings & Contacts Download Settings & Contacts
@@ -210,7 +221,7 @@
<a <a
ref="downloadLink" ref="downloadLink"
v-bind:class="computedDownloadLinkClassNames()" v-bind:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
If no download happened yet, click again here to download now. If no download happened yet, click again here to download now.
</a> </a>
@@ -291,7 +302,7 @@
<router-link <router-link
id="switch-identity-link" id="switch-identity-link"
:to="{ name: 'identity-switcher' }" :to="{ name: 'identity-switcher' }"
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md mb-2"
> >
Switch Identifier Switch Identifier
</router-link> </router-link>
@@ -299,7 +310,7 @@
<label <label
for="toggleShowAmounts" for="toggleShowAmounts"
class="flex items-center justify-between cursor-pointer my-4" class="flex items-center justify-between cursor-pointer my-4"
@click="handleChange" @click="toggleShowContactAmounts"
> >
<!-- label --> <!-- label -->
<span class="text-slate-500 text-sm font-bold">Contacts Display</span> <span class="text-slate-500 text-sm font-bold">Contacts Display</span>
@@ -439,6 +450,28 @@
{{ DEFAULT_PUSH_SERVER }} {{ DEFAULT_PUSH_SERVER }}
</span> </span>
<label
for="toggleShowShortcutBvc"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleShowShortcutBvc"
>
<!-- label -->
<span class="text-slate-500 text-sm font-bold"
>Show BVC Shortcut on Home Page</span
>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="showShortcutBvc" 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="mt-4"> <div class="mt-4">
<h2 class="text-slate-500 text-sm font-bold"> <h2 class="text-slate-500 text-sm font-bold">
Contacts & Settings Database Contacts & Settings Database
@@ -449,7 +482,7 @@
<input type="file" @change="uploadFile" class="ml-2" /> <input type="file" @change="uploadFile" class="ml-2" />
<div v-if="showContactImport()"> <div v-if="showContactImport()">
<button <button
class="block text-center text-md uppercase bg-blue-500 text-white px-1.5 py-2 rounded-md mb-6" class="block text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="submitFile()" @click="submitFile()"
> >
Import Settings & Contacts Import Settings & Contacts
@@ -464,7 +497,7 @@
<button> <button>
<router-link <router-link
:to="{ name: 'statistics' }" :to="{ name: 'statistics' }"
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-fit text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
> >
See Global Animated History of Giving See Global Animated History of Giving
</router-link> </router-link>
@@ -485,23 +518,25 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { AppString, DEFAULT_PUSH_SERVER } from "@/constants/app"; import {
AppString,
DEFAULT_IMAGE_API_SERVER,
DEFAULT_PUSH_SERVER,
NotificationIface,
} from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer"; import {
ErrorResponse,
EndorserRateLimits,
ImageRateLimits,
} from "@/libs/endorserServer";
// 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 Notification {
group: string;
type: string;
title: string;
text: string;
}
interface IAccount { interface IAccount {
did: string; did: string;
publicKeyHex: string; publicKeyHex: string;
@@ -513,7 +548,7 @@ const inputFileNameRef = ref<Blob>();
@Component({ components: { QuickNav, TopMessage } }) @Component({ components: { QuickNav, TopMessage } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
AppConstants = AppString; AppConstants = AppString;
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER; DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
@@ -523,7 +558,9 @@ export default class AccountViewView extends Vue {
apiServerInput = ""; apiServerInput = "";
derivationPath = ""; derivationPath = "";
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
endorserLimits: EndorserRateLimits | null = null;
givenName = ""; givenName = "";
imageLimits: ImageRateLimits | null = null;
isRegistered = false; isRegistered = false;
isSubscribed = false; isSubscribed = false;
notificationMaybeChanged = false; notificationMaybeChanged = false;
@@ -531,7 +568,6 @@ export default class AccountViewView extends Vue {
publicBase64 = ""; publicBase64 = "";
webPushServer = ""; webPushServer = "";
webPushServerInput = ""; webPushServerInput = "";
limits: RateLimits | null = null;
limitsMessage = ""; limitsMessage = "";
loadingLimits = false; loadingLimits = false;
showContactGives = false; showContactGives = false;
@@ -540,6 +576,7 @@ export default class AccountViewView extends Vue {
showB64Copy = false; showB64Copy = false;
showPubCopy = false; showPubCopy = false;
showAdvanced = false; showAdvanced = false;
showShortcutBvc = false;
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;
warnIfProdServer = false; warnIfProdServer = false;
warnIfTestServer = false; warnIfTestServer = false;
@@ -599,6 +636,7 @@ export default class AccountViewView extends Vue {
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer; this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer; this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || ""; this.webPushServer = (settings?.webPushServer as string) || "";
@@ -656,7 +694,7 @@ export default class AccountViewView extends Vue {
.then(() => setTimeout(fn, 2000)); .then(() => setTimeout(fn, 2000));
} }
handleChange() { toggleShowContactAmounts() {
this.showContactGives = !this.showContactGives; this.showContactGives = !this.showContactGives;
this.updateShowContactAmounts(); this.updateShowContactAmounts();
} }
@@ -671,7 +709,12 @@ export default class AccountViewView extends Vue {
this.updateWarnIfTestServer(this.warnIfTestServer); this.updateWarnIfTestServer(this.warnIfTestServer);
} }
readableTime(timeStr: string) { toggleShowShortcutBvc() {
this.showShortcutBvc = !this.showShortcutBvc;
this.updateShowShortcutBvc(this.showShortcutBvc);
}
readableDate(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T")); return timeStr.substring(0, timeStr.indexOf("T"));
} }
@@ -766,7 +809,7 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after contact setting update because:", "Telling user to try again after contact-amounts setting update because:",
err, err,
); );
} }
@@ -789,7 +832,7 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after setting update because:", "Telling user to try again after prod-server-warning setting update because:",
err, err,
); );
} }
@@ -812,7 +855,30 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after setting update because:", "Telling user to try again after test-server-warning setting update because:",
err,
);
}
}
public async updateShowShortcutBvc(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating BVC Shortcut Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after BVC-shortcut setting update because:",
err, err,
); );
} }
@@ -989,11 +1055,11 @@ export default class AccountViewView extends Vue {
this.limitsMessage = ""; this.limitsMessage = "";
try { try {
const resp = await this.fetchRateLimits(identity); const resp = await this.fetchEndorserRateLimits(identity);
if (resp.status === 200) { if (resp.status === 200) {
this.limits = resp.data; this.endorserLimits = resp.data;
if (!this.isRegistered) { if (!this.isRegistered) {
// the user is not known to be registered, but they are so let's record it // the user was not known to be registered, but now they are (because we got no error) so let's record it
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
@@ -1013,6 +1079,10 @@ export default class AccountViewView extends Vue {
); );
} }
} }
const imageResp = await this.fetchImageRateLimits(identity);
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
}
} }
} catch (error) { } catch (error) {
this.handleRateLimitsError(error); this.handleRateLimitsError(error);
@@ -1033,17 +1103,29 @@ export default class AccountViewView extends Vue {
} }
/** /**
* Fetches rate limits from the server. * Fetches rate limits from the Endorser server.
* *
* @param {IIdentifier} identity - The identity object to check rate limits for. * @param {IIdentifier} identity - The identity object to check rate limits for.
* @returns {Promise<AxiosResponse>} The Axios response object. * @returns {Promise<AxiosResponse>} The Axios response object.
*/ */
private async fetchRateLimits(identity: IIdentifier) { private async fetchEndorserRateLimits(identity: IIdentifier) {
const url = `${this.apiServer}/api/report/rateLimits`; const url = `${this.apiServer}/api/report/rateLimits`;
const headers = await this.getHeaders(identity); const headers = await this.getHeaders(identity);
return await this.axios.get(url, { headers } as AxiosRequestConfig); return await this.axios.get(url, { headers } as AxiosRequestConfig);
} }
/**
* Fetches rate limits from the image server.
*
* @param {IIdentifier} identity - The identity object to check rate limits for.
* @returns {Promise<AxiosResponse>} The Axios response object.
*/
private async fetchImageRateLimits(identity: IIdentifier) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
const headers = await this.getHeaders(identity);
return await this.axios.get(url, { headers } as AxiosRequestConfig);
}
/** /**
* Handles errors that occur while fetching rate limits. * Handles errors that occur while fetching rate limits.
* *

View File

@@ -123,7 +123,7 @@
<div class="columns-3"> <div class="columns-3">
<button <button
class="col-span-1 bg-blue-600 text-white px-4 py-2 rounded-md" class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
v-if=" v-if="
libsUtil.isGiveRecordTheUserCanConfirm( libsUtil.isGiveRecordTheUserCanConfirm(
veriClaim, veriClaim,
@@ -140,7 +140,7 @@
<button <button
v-if="libsUtil.canFulfillOffer(veriClaim)" v-if="libsUtil.canFulfillOffer(veriClaim)"
@click="openFulfillGiftDialog()" @click="openFulfillGiftDialog()"
class="col-span-1 block w-fit text-center text-md bg-blue-600 text-white px-1.5 py-2 rounded-md" class="col-span-1 block w-fit text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
Affirm Delivery Affirm Delivery
<fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" /> <fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" />
@@ -377,7 +377,7 @@
</p> </p>
<button <button
v-else v-else
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
@click="showFullClaim(veriClaim.id as string)" @click="showFullClaim(veriClaim.id as string)"
> >
Load Full Claim Details Load Full Claim Details
@@ -390,7 +390,7 @@
<a <a
:href="apiServer + '/api/claim/' + veriClaim.id" :href="apiServer + '/api/claim/' + veriClaim.id"
target="_blank" target="_blank"
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
> >
View on the Public Server View on the Public Server
</a> </a>
@@ -407,6 +407,7 @@ import { useClipboard } from "@vueuse/core";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@@ -418,18 +419,11 @@ import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { GiverInputInfo } from "@/libs/endorserServer"; import { GiverInputInfo } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav }, components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
}) })
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null"; accountIdentityStr: string = "null";
activeDid = ""; activeDid = "";
@@ -736,10 +730,7 @@ export default class ClaimView extends Vue {
), ),
), ),
); );
const confirmationClaim: serverUtil.GenericVerifiableCredential & { const confirmationClaim: serverUtil.GenericVerifiableCredential = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any;
} = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,

View File

@@ -32,12 +32,12 @@
<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-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
Cancel Cancel
</button> </button>

View File

@@ -105,9 +105,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda"; 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 QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
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";
@@ -118,21 +123,10 @@ import {
GiveVerifiableCredential, GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT, SCHEMA_ORG_CONTEXT,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as didJwt from "did-jwt";
import { AxiosError } from "axios";
import QuickNav from "@/components/QuickNav.vue";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue { export default class ContactAmountssView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
@@ -185,7 +179,7 @@ export default class ContactAmountssView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings or gives.", err); console.error("Error retrieving settings or gives.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -32,7 +32,7 @@
<button <button
type="button" type="button"
@click="openDialog()" @click="openDialog()"
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md" class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
> >
<fa icon="gift" class="fa-fw"></fa> <fa icon="gift" class="fa-fw"></fa>
</button> </button>
@@ -57,7 +57,7 @@
<button <button
type="button" type="button"
@click="openDialog(contact)" @click="openDialog(contact)"
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md" class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
> >
<fa icon="gift" class="fa-fw"></fa> <fa icon="gift" class="fa-fw"></fa>
</button> </button>
@@ -76,29 +76,24 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { IIdentifier } from "@veramo/core";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account, AccountsSchema } from "@/db/tables/accounts"; import { Account, AccountsSchema } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { GiverInputInfo } from "@/libs/endorserServer"; import { GiverInputInfo } from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
}) })
export default class ContactGiftingView extends Vue { export default class ContactGiftingView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -118,13 +113,13 @@ export default class ContactGiftingView extends Vue {
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.orderBy("name").toArray();
localStorage.removeItem("projectId"); localStorage.removeItem("projectId");
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings & contacts:", err); console.error("Error retrieving settings & contacts:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -18,19 +18,22 @@
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
Your Contact Info Your Contact Info
</h1> </h1>
<p v-if="!givenName" class="text-center mt-2"> <p
v-if="!givenName"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
>
<span class="text-red">Beware!</span> <span class="text-red">Beware!</span>
You aren't sharing your name, so hurry and You aren't sharing your name, so quickly
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
class="bg-blue-500 text-white px-1.5 py-1 rounded-md" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"
> >
go here to set it for them. click here to set it for them.
</router-link> </router-link>
</p> </p>
</div> </div>
<div @click="onCopyToClipboard()" v-if="activeDid"> <div @click="onCopyToClipboard()" v-if="activeDid" class="text-center">
<!-- <!--
Play with display options: https://qr-code-styling.com/ Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3 See docs: https://www.npmjs.com/package/qr-code-generator-vue3
@@ -41,9 +44,7 @@
:dotsOptions="{ type: 'square' }" :dotsOptions="{ type: 'square' }"
class="flex justify-center" class="flex justify-center"
/> />
<span class="flex justify-center"> <span> Click QR to copy your contact URL to your clipboard. </span>
Click QR to copy your contact URL to your clipboard.
</span>
</div> </div>
<div class="text-center" v-else> <div class="text-center" v-else>
You have no identitifiers yet, so You have no identitifiers yet, so
@@ -77,6 +78,7 @@ import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader"; import { QrcodeStream } from "vue-qrcode-reader";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto"; import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto";
@@ -90,13 +92,6 @@ import {
// 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 Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: {
QrcodeStream, QrcodeStream,
@@ -105,7 +100,7 @@ interface Notification {
}, },
}) })
export default class ContactQRScanShow extends Vue { export default class ContactQRScanShow extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
@@ -185,7 +180,6 @@ export default class ContactQRScanShow extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) { onScanDetect(content: any) {
if (content[0]?.rawValue) { if (content[0]?.rawValue) {
//console.log("onDetect", content[0].rawValue);
localStorage.setItem("contactEndorserUrl", content[0].rawValue); localStorage.setItem("contactEndorserUrl", content[0].rawValue);
this.$router.push({ name: "contacts" }); this.$router.push({ name: "contacts" });
} else { } else {
@@ -203,7 +197,7 @@ export default class ContactQRScanShow extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanError(error: any) { onScanError(error: any) {
console.log("Scan was invalid:", error); console.error("Scan was invalid:", error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -67,12 +67,12 @@
<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-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
value="Look Up Contact" value="Look Up 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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
Cancel Cancel
</button> </button>

View File

@@ -11,7 +11,7 @@
<span> <span>
<a <a
@click="showHintsForOnboarding()" @click="showHintsForOnboarding()"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1" class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
> >
Onboarding Guide Onboarding Guide
</a> </a>
@@ -22,7 +22,7 @@
<div class="mt-4 mb-4 flex items-stretch"> <div class="mt-4 mb-4 flex items-stretch">
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md" class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
> >
<fa icon="qrcode" class="fa-fw text-2xl" /> <fa icon="qrcode" class="fa-fw text-2xl" />
</router-link> </router-link>
@@ -75,7 +75,7 @@
<br /> <br />
(Only most recent hours included. To see more, click (Only most recent hours included. To see more, click
<span <span
class="text-sm uppercase bg-slate-500 text-white px-1 py-1 rounded-md" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
> >
<fa icon="file-lines" class="fa-fw" /> <fa icon="file-lines" class="fa-fw" />
</span> </span>
@@ -100,7 +100,7 @@
></EntityIcon> ></EntityIcon>
{{ contact.name || AppString.NO_CONTACT_NAME }} {{ contact.name || AppString.NO_CONTACT_NAME }}
<button <button
class="text-sm uppercase bg-slate-500 text-white px-1 rounded-md" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
@click=" @click="
contactEdit = contact; contactEdit = contact;
contactNewName = contact.name; contactNewName = contact.name;
@@ -137,7 +137,7 @@
<div v-if="activeDid"> <div v-if="activeDid">
<button <button
v-if="contact.seesMe" v-if="contact.seesMe"
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="setVisibility(contact, false, true)" @click="setVisibility(contact, false, true)"
title="They can see you" title="They can see you"
> >
@@ -145,14 +145,14 @@
</button> </button>
<button <button
v-else v-else
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="setVisibility(contact, true, true)" @click="setVisibility(contact, true, true)"
title="They cannot see you" title="They cannot see you"
> >
<fa icon="eye-slash" class="fa-fw" /> <fa icon="eye-slash" class="fa-fw" />
</button> </button>
<button <button
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)" @click="checkVisibility(contact)"
title="Check Visibility" title="Check Visibility"
v-if="activeDid" v-if="activeDid"
@@ -161,7 +161,7 @@
</button> </button>
<button <button
@click="register(contact)" @click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 px-2 py-1.5 rounded-md"
v-if="activeDid" v-if="activeDid"
title="Registration" title="Registration"
> >
@@ -176,7 +176,7 @@
<button <button
@click="deleteContact(contact)" @click="deleteContact(contact)"
class="text-sm uppercase bg-red-600 text-white ml-24 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-24 px-2 py-1.5 rounded-md"
title="Delete" title="Delete"
> >
<fa icon="trash-can" class="fa-fw" /> <fa icon="trash-can" class="fa-fw" />
@@ -187,7 +187,7 @@
class="ml-auto flex gap-1.5" class="ml-auto flex gap-1.5"
> >
<button <button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md" class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
@click="onClickAddGive(activeDid, contact.did)" @click="onClickAddGive(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''" :title="givenByMeDescriptions[contact.did] || ''"
> >
@@ -229,7 +229,7 @@
name: 'contact-amounts', name: 'contact-amounts',
query: { contactDid: contact.did }, query: { contactDid: contact.did },
}" }"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
title="See more given activity" title="See more given activity"
> >
<fa icon="file-lines" class="fa-fw" /> <fa icon="file-lines" class="fa-fw" />
@@ -284,6 +284,7 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { IndexableType } from "dexie";
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";
@@ -311,7 +312,6 @@ import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { IndexableType } from "dexie";
// 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;
@@ -364,11 +364,7 @@ export default class ContactsView extends Vue {
if (this.showGiveNumbers) { if (this.showGiveNumbers) {
this.loadGives(); this.loadGives();
} }
const allContacts = await db.contacts.toArray(); this.contacts = await db.contacts.orderBy("name").toArray();
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
if (this.contactEndorserUrl) { if (this.contactEndorserUrl) {
await this.addContactFromScan(this.contactEndorserUrl); await this.addContactFromScan(this.contactEndorserUrl);
@@ -501,7 +497,7 @@ export default class ContactsView extends Vue {
this.givenToMeConfirmed = givenToMeConfirmed; this.givenToMeConfirmed = givenToMeConfirmed;
this.givenToMeUnconfirmed = givenToMeUnconfirmed; this.givenToMeUnconfirmed = givenToMeUnconfirmed;
} catch (error) { } catch (error) {
console.log("Error loading gives", error); console.error("Error loading gives", error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -576,11 +572,7 @@ export default class ContactsView extends Vue {
-1, -1,
); );
} }
const allContacts = await db.contacts.toArray(); this.contacts = await db.contacts.orderBy("name").toArray();
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
return; return;
} }
@@ -876,7 +868,7 @@ export default class ContactsView extends Vue {
(contact.name || "That unnamed person") + (contact.name || "That unnamed person") +
" has been registered.", " has been registered.",
}, },
-1, 5000,
); );
} }
} catch (error) { } catch (error) {
@@ -1008,7 +1000,7 @@ export default class ContactsView extends Vue {
-1, -1,
); );
} else { } else {
console.log("Got bad server response when checking visibility: ", resp); console.error("Got bad server response checking visibility:", resp);
const message = resp.data.error?.message || "Got bad server response."; const message = resp.data.error?.message || "Got bad server response.";
this.$notify( this.$notify(
{ {
@@ -1021,7 +1013,7 @@ export default class ContactsView extends Vue {
); );
} }
} catch (err) { } catch (err) {
console.log("Caught error from request to check visibility:", err); console.error("Caught error from request to check visibility:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -1034,12 +1026,6 @@ export default class ContactsView extends Vue {
} }
} }
// from https://stackoverflow.com/a/175787/845494
//
private isNumeric(str: string): boolean {
return !isNaN(+str);
}
private nameForDid(contacts: Array<Contact>, did: string): string { private nameForDid(contacts: Array<Contact>, did: string): string {
const contact = R.find((con) => con.did == did, contacts); const contact = R.find((con) => con.did == did, contacts);
return this.nameForContact(contact); return this.nameForContact(contact);
@@ -1075,7 +1061,7 @@ export default class ContactsView extends Vue {
return; return;
} }
} }
if (!this.isNumeric(this.hourInput)) { if (!libsUtil.isNumeric(this.hourInput)) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -1198,7 +1184,7 @@ export default class ContactsView extends Vue {
title: "Done", title: "Done",
text: "Successfully logged time to the server.", text: "Successfully logged time to the server.",
}, },
-1, 5000,
); );
if (fromDid === identity.did) { if (fromDid === identity.did) {
@@ -1212,7 +1198,7 @@ export default class ContactsView extends Vue {
} }
} }
} catch (error) { } catch (error) {
console.log("Error in createAndSubmitContactGive: ", error); console.error("Error in createAndSubmitContactGive: ", error);
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError; const serverError = error as AxiosError;
if (serverError) { if (serverError) {

View File

@@ -129,23 +129,17 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { didInfo, PlanData } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
interface Notification { import { accountsDB, db } from "@/db/index";
group: string; import { Contact } from "@/db/tables/contacts";
type: string; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
title: string; import { accessToken } from "@/libs/crypto";
text: string; import { didInfo, PlanData } from "@/libs/endorserServer";
}
@Component({ @Component({
components: { components: {
@@ -157,7 +151,7 @@ interface Notification {
}, },
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -260,7 +254,7 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.log("Problem with full search:", details); console.error("Problem with full search:", details);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -288,7 +282,7 @@ export default class DiscoverView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
console.log("Error with feed load:", e); console.error("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -343,7 +337,7 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.log("Problem with nearby search:", details); console.error("Problem with nearby search:", details);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -380,7 +374,7 @@ export default class DiscoverView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
console.log("Error with feed load:", e); console.error("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

428
src/views/GiftedDetails.vue Normal file
View File

@@ -0,0 +1,428 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancel()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1>
<h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giverName || "somebody not named" }}
</h1>
<textarea
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received"
v-model="description"
/>
<div class="flex flex-row justify-center">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()"
>
{{ libsUtil.UNIT_SHORT[unitCode] }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<fa icon="chevron-left" />
</div>
<input
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<fa icon="chevron-right" />
</div>
</div>
<div class="flex justify-center mt-4">
<span v-if="imageUrl" class="flex justify-between">
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
<img :src="imageUrl" class="h-24 rounded-xl" />
</a>
<fa
icon="trash-can"
@click="confirmDeleteImage"
class="text-red-500 fa-fw ml-8 mt-10"
/>
</span>
<span v-else>
<fa
icon="camera"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
@click="openPhotoDialog"
/>
</span>
</div>
<GiftedPhotoDialog ref="photoDialog" />
<div v-if="projectId" class="mt-4">
<fa
icon="check"
class="bg-slate-500 text-white h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
/>
<label class="text-sm">This is given to a project</label>
</div>
<div v-if="!projectId" class="mt-4">
<input type="checkbox" class="h-6 w-6 mr-2" v-model="givenToUser" />
<label class="text-sm">Given to you</label>
</div>
<div class="mt-4">
<input type="checkbox" class="h-6 w-6 mr-2" v-model="isTrade" />
<label class="text-sm">Trade (not a gift)</label>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
</p>
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel"
>
Cancel
</button>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { createAndSubmitGive } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { accessToken } from "@/libs/crypto";
import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue";
@Component({
components: {
GiftedDialog,
GiftedPhotoDialog,
QuickNav,
TopMessage,
},
})
export default class GiftedDetails extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
apiServer = "";
amountInput = "0";
description = "";
givenToUser = false;
giverDid: string | undefined;
giverName = "";
imageUrl = "";
isTrade = false;
message = "";
offerId = "";
projectId = "";
unitCode = "HUR";
libsUtil = libsUtil;
async mounted() {
this.amountInput = this.$route.query.amountInput as string;
this.description = this.$route.query.description as string;
this.giverDid = this.$route.query.giverDid as string;
this.giverName = this.$route.query.giverName as string;
this.message = this.$route.query.message as string;
this.offerId = this.$route.query.offerId as string;
this.projectId = this.$route.query.projectId as string;
this.unitCode = this.$route.query.unitCode as string;
this.imageUrl = localStorage.getItem("imageUrl") || "";
this.givenToUser = !this.projectId;
try {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your settings.",
},
-1,
);
}
}
changeUnitCode() {
const units = Object.keys(this.libsUtil.UNIT_SHORT);
const index = units.indexOf(this.unitCode);
this.unitCode = units[(index + 1) % units.length];
}
increment() {
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
}
decrement() {
this.amountInput = `${Math.max(
0,
(parseFloat(this.amountInput) || 1) - 1,
)}`;
}
cancel() {
this.deleteImage(); // not awaiting, so they'll go back immediately
this.$router.back();
}
openPhotoDialog() {
(this.$refs.photoDialog as GiftedPhotoDialog).open((imgUrl) => {
this.imageUrl = imgUrl;
});
}
confirmDeleteImage() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Are you sure you want to delete the image?",
text: "",
onYes: this.deleteImage,
},
-1,
);
}
async deleteImage() {
if (!this.imageUrl) {
return;
}
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const token = await accessToken(identity);
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
encodeURIComponent(this.imageUrl),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
if (response.status === 204) {
// don't bother with a notification
// (either they'll simply continue or they're canceling and going back)
} else {
console.error("Non-success deleting image:", response);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem deleting the image.",
},
5000,
);
// keep the imageUrl in localStorage so the user can try again if they want
return;
}
localStorage.removeItem("imageUrl");
this.imageUrl = "";
} catch (error) {
console.error("Error deleting image:", error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).response.status === 404) {
console.log("The image was already deleted:", error);
localStorage.removeItem("imageUrl");
this.imageUrl = "";
// it already doesn't exist so we won't say anything to the user
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error deleting the image.",
},
5000,
);
}
}
}
async confirm() {
this.$notify(
{
group: "alert",
type: "toast",
text: "Recording the give...",
title: "",
},
1000,
);
// this is asynchronous, but we don't need to wait for it to complete
await this.recordGive();
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param amountInput may be 0
* @param unitCode may be omitted, defaults to "HUR"
*/
public async recordGive() {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record a give.",
},
-1,
);
return;
}
if (!this.description && !this.amountInput) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${
this.libsUtil.UNIT_LONG[this.unitCode]
}.`,
},
-1,
);
return;
}
try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
this.giverDid,
this.givenToUser ? this.activeDid : undefined,
this.description,
parseFloat(this.amountInput),
this.unitCode,
this.projectId,
this.offerId,
this.isTrade,
this.imageUrl,
);
if (
result.type === "error" ||
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result);
console.error("Error with give creation result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: errorMessage || "There was an error creating the give.",
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
},
5000,
);
localStorage.removeItem("imageUrl");
this.$router.back();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error with give recordation caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the give.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: message,
},
-1,
);
}
}
// Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
);
}
}
</script>

View File

@@ -31,7 +31,7 @@
If this works then you're all set. If this works then you're all set.
<button <button
@click="sendTestWebPushMessage(true)" @click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
> >
Send Yourself a Test Web Push Message (Through Push Server but Send Yourself a Test Web Push Message (Through Push Server but
Skipping Client Filter) Skipping Client Filter)
@@ -233,7 +233,7 @@
<h2 class="text-xl font-semibold mt-4">Tests</h2> <h2 class="text-xl font-semibold mt-4">Tests</h2>
<button <button
@click="showTestNotification()" @click="showTestNotification()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2" class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Test Notification Directly to Device (Not Through Push Server) Send Test Notification Directly to Device (Not Through Push Server)
</button> </button>
@@ -246,7 +246,7 @@
<button <button
@click="alertWebPushSubscription()" @click="alertWebPushSubscription()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2" class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Show Web Push Subscription Info Show Web Push Subscription Info
</button> </button>
@@ -259,7 +259,7 @@
<button <button
@click="sendTestWebPushMessage(true)" @click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2" class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Yourself a Test Web Push Message (Through Push Server but Skipping Send Yourself a Test Web Push Message (Through Push Server but Skipping
Client Filter) Client Filter)
@@ -272,7 +272,7 @@
<button <button
@click="sendTestWebPushMessage()" @click="sendTestWebPushMessage()"
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2" class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2"
> >
Send Yourself a Test Web Push Message (Through Push Server and Client Send Yourself a Test Web Push Message (Through Push Server and Client
Filter) Filter)
@@ -294,18 +294,12 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { sendTestThroughPushServer } from "@/libs/util"; import { sendTestThroughPushServer } from "@/libs/util";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class HelpNotificationsView extends Vue { export default class HelpNotificationsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()" @click="$router.back()"
> >
<fa icon="chevron-left" class="fa-fw"></fa> <fa icon="chevron-left" class="fa-fw" />
</h1> </h1>
</div> </div>
@@ -217,6 +217,28 @@
</ul> </ul>
<p>To erase your data from our servers, contact us (below).</p> <p>To erase your data from our servers, contact us (below).</p>
<h2 class="text-xl font-semibold">
How do I get higher limits?
</h2>
<p>
Let's talk. Contact us (below).
</p>
<h2 class="text-xl font-semibold">
How do I access even more functionality?
</h2>
<p>
There is an "Advanced" section at the bottom of the Account
<fa icon="circle-user" /> page.
</p>
<p>
There is a even more functionality in a mobile app (and more
documentation) at
<a href="https://endorser.ch" class="text-blue-500">
EndorserSearch.com
</a>
</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>
@@ -240,35 +262,64 @@
</h2> </h2>
<p> <p>
<router-link class="text-blue-500" to="/help-notifications" <router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link >Here.</router-link
> >
</p> </p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I get higher limits? My app is misbehaving, like showing me a blank screen or failing to show a feed.
What can I do?
</h2> </h2>
<p> <p>
Let's talk. Contact us (below). First, note that clearing the cache will clear all your identity and contact info,
so we recommend doing other things first (unless you know you have your backups ready).
</p> </p>
<ul class="list-disc list-outside ml-4">
<h2 class="text-xl font-semibold"> <li>
How do I access even more functionality? Drag down on the screen to refresh it; do that multiple times, because
</h2> it sometimes takes multiple tries for the app to refresh to the current version.
You can see the version information at the bottom of this page; the best
way to determine the current version is to open this page in an incognito
browser window and look at the version there.
</li>
<li>
Close all tabs that have Time Safari open; it can be difficult to find them all,
and you may have to close all your tabs. In addition, it may be running as an
installed app, so look for any Time Safari app that may be running outside a browser.
</li>
<li>
It can help to reregister the service worker:
<ul>
<li>
In Chrome, open a tab to
"chrome://serviceworker-internals",
find "timesafari.app", and click "Unregister".</li>
<li>
In Firefox,
open a tab to "about:serviceworkers",
find "timesafari.app", and click "Unregister".
</li>
<li>
<a href="https://duckduckgo.com/?q=unregister+service+worker" class="text-blue-500">Search</a>
for instructions for other browsers.</li>
</ul>
Then reload Time Safari.
</li>
<li>
Restart your device.
</li>
</ul>
<p> <p>
There is an "Advanced" section at the bottom of the Account If you still have problems, you can clear the cache and even uninstall
<fa icon="circle-user" /> page. and reinstall the app, but be sure to have your backups ready or be
</p> prepared to restart with a new identity and recreate your network.
<p> Nobody else has access to your identity or contact information because
There is a even more functionality in a mobile app (and more this app is designed to give you full control over your data.
documentation) at
<a href="https://endorser.ch" class="text-blue-500">
EndorserSearch.com
</a>
</p> </p>
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2> <h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
<p style="display:inline; align-items: center"> <p style="display:inline; align-items: center">
This work is marked with This work is public domain, governed by
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer"> <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
<span class="text-blue-500 mr-1">CC0 1.0</span> <span class="text-blue-500 mr-1">CC0 1.0</span>
<img <img
@@ -325,18 +376,12 @@ import { Component, Vue } from "vue-facing-decorator";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { ONBOARD_MESSAGE } from "@/libs/util"; import { ONBOARD_MESSAGE } from "@/libs/util";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
package = Package; package = Package;
commitHash = process.env.VUE_APP_GIT_HASH; commitHash = process.env.VUE_APP_GIT_HASH;

View File

@@ -1,10 +1,10 @@
<template> <template>
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home" />
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8">
Time Safari Time Safari
</h1> </h1>
@@ -30,7 +30,7 @@
and go click on that new app. and go click on that new app.
</span> </span>
<span <span
v-else-if="userAgentInfo.getBrowser().name.startsWith('Chrome')" v-else-if="userAgentInfo.getBrowser()?.name?.startsWith('Chrome')"
> >
You should see a prompt to install, or you can click on the You should see a prompt to install, or you can click on the
top-right dots top-right dots
@@ -59,6 +59,15 @@
</div> </div>
</div> </div>
<div v-if="showShortcutBvc" class="mb-4">
<router-link
:to="{ name: 'quick-action-bvc' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Bountiful Voluntaryist Community Actions</router-link
>
</div>
<!-- show the actions for recognizing a give --> <!-- show the actions for recognizing a give -->
<div class="mb-8"> <div class="mb-8">
<div v-if="isCreatingIdentifier"> <div v-if="isCreatingIdentifier">
@@ -76,7 +85,7 @@
</p> </p>
<router-link <router-link
:to="{ name: 'start' }" :to="{ name: 'start' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
> >
Create An Identifier</router-link Create An Identifier</router-link
> >
@@ -90,7 +99,7 @@
giving. giving.
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
> >
Show Them Your Identifier Info</router-link Show Them Your Identifier Info</router-link
> >
@@ -105,14 +114,8 @@
<div v-else> <div v-else>
<!-- activeDid && isRegistered --> <!-- activeDid && isRegistered -->
<div class="flex justify-between mb-4"> <div class="mb-4">
<h2 class="text-xl font-bold">Record Something Given</h2> <h2 class="text-xl font-bold">Record Something Given By:</h2>
<button
@click="openGiftedPrompts()"
class="block text-center text-md font-bold bg-blue-500 text-white px-2 py-3 rounded-md"
>
Ideas...
</button>
</div> </div>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
@@ -145,21 +148,20 @@
</li> </li>
</ul> </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) --> <div class="flex justify-between">
<router-link <router-link
v-if="allContacts.length >= 7" v-if="allContacts.length >= 7"
:to="{ name: 'contact-gives' }" :to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md" class="block text-center text-md font-bold uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
> >
Show More Contacts&hellip; Choose From All Contacts
</router-link> </router-link>
<button
<!-- If there are no contacts, show this instead: --> @click="openGiftedPrompts()"
<div class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500" >
v-if="allContacts.length === 0" Ideas...
> </button>
(No contacts to show.)
</div> </div>
</div> </div>
</div> </div>
@@ -185,16 +187,24 @@
class="border-b border-dashed border-slate-400 text-orange-400 pb-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 == feedLastViewedClaimId" v-if="record.jwtId == feedLastViewedClaimId"
> >
You've seen all the following before You've already seen all the following
</div> </div>
<div class="grid grid-cols-12"> <div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start"> <span class="col-span-11 justify-self-start">
<fa <span>
icon="gift" <fa
class="col-span-1 pt-1 pr-2 text-slate-500" v-if="record.giver.known || record.receiver.known"
></fa> icon="circle-user"
{{ this.giveDescription(record) }} class="col-span-1 pt-1 pl-0 pr-3 text-slate-500"
/>
<fa
v-else
icon="gift"
class="col-span-1 pt-1 pl-3 pr-0 text-slate-500"
/>
</span>
{{ giveDescription(record) }}
<a @click="onClickLoadClaim(record.jwtId)"> <a @click="onClickLoadClaim(record.jwtId)">
<fa <fa
icon="circle-info" icon="circle-info"
@@ -215,6 +225,11 @@
</router-link> </router-link>
</span> </span>
</div> </div>
<div v-if="record.image" class="flex justify-center">
<a :href="record.image" target="_blank">
<img :src="record.image" class="h-24 mt-2 rounded-xl" />
</a>
</div>
</li> </li>
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
@@ -229,6 +244,7 @@
<script lang="ts"> <script lang="ts">
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@@ -237,24 +253,30 @@ import GiftedPrompts from "@/components/GiftedPrompts.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
didInfo, contactForDid,
didInfoForContact,
GiverInputInfo, GiverInputInfo,
GiveServerRecord, GiveServerRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { IIdentifier } from "@veramo/core";
import { generateSaveAndActivateIdentity } from "@/libs/util"; import { generateSaveAndActivateIdentity } from "@/libs/util";
interface Notification { interface GiveRecordWithContactInfo extends GiveServerRecord {
group: string; giver: {
type: string; displayName: string;
title: string; known: boolean;
text: string; };
image: string;
receiver: {
displayName: string;
known: boolean;
};
} }
@Component({ @Component({
@@ -268,18 +290,19 @@ interface Notification {
}, },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
feedData: GiveServerRecord[] = []; feedData: GiveRecordWithContactInfo[] = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedClaimId?: string;
isCreatingIdentifier = false; isCreatingIdentifier = false;
isFeedLoading = true; isFeedLoading = true;
isRegistered = false; isRegistered = false;
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
@@ -314,6 +337,7 @@ export default class HomeView extends Vue {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.showShortcutBvc = !!settings?.showShortcutBvc;
if (this.allMyDids.length === 0) { if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true; this.isCreatingIdentifier = true;
@@ -328,7 +352,7 @@ export default class HomeView extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings or feed.", err); console.error("Error retrieving settings or feed.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -388,7 +412,38 @@ export default class HomeView extends Vue {
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId) await this.retrieveGives(this.apiServer, 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); // include the descriptions of the giver and receiver
const newFeedData: GiveRecordWithContactInfo = results.data.map(
(record: GiveServerRecord) => {
// similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claim = (record.fullClaim as any).claim || record.fullClaim;
// agent.did is for legacy data, before March 2023
const giverDid =
claim.agent?.identifier || (claim.agent as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
// recipient.did is for legacy data, before March 2023
const recipientDid =
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
return {
...record,
giver: didInfoForContact(
giverDid,
this.activeDid,
contactForDid(giverDid, this.allContacts),
this.allMyDids,
),
image: claim.image,
receiver: didInfoForContact(
recipientDid,
this.activeDid,
contactForDid(recipientDid, this.allContacts),
this.allMyDids,
),
};
},
);
this.feedData = this.feedData.concat(newFeedData);
this.feedPreviousOldestId = this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId; results.data[results.data.length - 1].jwtId;
// The following update is only done on the first load. // The following update is only done on the first load.
@@ -404,7 +459,7 @@ export default class HomeView extends Vue {
} }
}) })
.catch((e) => { .catch((e) => {
console.log("Error with feed load:", e); console.error("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -436,7 +491,7 @@ export default class HomeView extends Vue {
}, },
); );
if (response.status !== 200) { if (!response.ok) {
throw await response.text(); throw await response.text();
} }
@@ -449,46 +504,52 @@ export default class HomeView extends Vue {
} }
} }
giveDescription(giveRecord: GiveServerRecord) { giveDescription(giveRecord: GiveRecordWithContactInfo) {
// similar code is in endorser-mobile utility.ts // similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential // claim.claim happen for some claims wrapped in a Verifiable Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim; const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
// agent.did is for legacy data, before March 2023
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did;
const giverInfo = didInfo(
giverDid,
this.activeDid,
this.allMyDids,
this.allContacts,
);
let gaveAmount = claim.object?.amountOfThisGood let gaveAmount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: ""; : "";
if (claim.description) { if (claim.description) {
if (gaveAmount) { if (gaveAmount) {
gaveAmount = gaveAmount + ", and also: "; gaveAmount = " (and " + gaveAmount + ")";
} }
gaveAmount = gaveAmount + claim.description; gaveAmount = claim.description + gaveAmount;
} }
if (!gaveAmount) { if (!gaveAmount) {
gaveAmount = "something not described"; gaveAmount = "something not described";
} }
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = /**
// eslint-disable-next-line @typescript-eslint/no-explicit-any * Only show giver and/or receiver info first if they're named.
claim.recipient?.identifier || (claim.recipient as any)?.did; * - If only giver is named, show "... gave"
const gaveRecipientInfo = gaveRecipientId * - If only receiver is named, show "... received"
? " to " + */
didInfo(
gaveRecipientId, const giverInfo = giveRecord.giver;
this.activeDid, const recipientInfo = giveRecord.receiver;
this.allMyDids, if (giverInfo.known && recipientInfo.known) {
this.allContacts, // both giver and recipient are named
) return `${giverInfo.displayName} gave to ${recipientInfo.displayName}: ${gaveAmount}`;
: ""; } else if (giverInfo.known) {
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount; // giver is named but recipient is not
return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`;
} else if (recipientInfo.known) {
// recipient is named but giver is not
return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`;
} else {
// neither giver nor recipient are named
let peopleInfo;
if (giverInfo.displayName === recipientInfo.displayName) {
peopleInfo = `between two who are ${giverInfo.displayName}`;
} else {
peopleInfo = `from ${giverInfo.displayName} to ${recipientInfo.displayName}`;
}
return gaveAmount + " (" + peopleInfo + ")";
}
} }
onClickLoadClaim(jwtId: string) { onClickLoadClaim(jwtId: string) {
@@ -506,7 +567,7 @@ export default class HomeView extends Vue {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
openDialog(giver: GiverInputInfo) { openDialog(giver?: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver); (this.$refs.customDialog as GiftedDialog).open(giver);
} }

View File

@@ -65,13 +65,13 @@
<router-link <router-link
id="start-link" id="start-link"
:to="{ name: 'start' }" :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" class="block text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
> >
Add Another Identity&hellip; Add Another Identity&hellip;
</router-link> </router-link>
<a <a
href="#" href="#"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-8"
@click="switchAccount('0')" @click="switchAccount('0')"
> >
No Identity No Identity
@@ -80,22 +80,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import { AppString, NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue { export default class IdentitySwitcherView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
Constants = AppString; Constants = AppString;
public accounts: typeof AccountsSchema; public accounts: typeof AccountsSchema;

View File

@@ -58,14 +58,14 @@
<div class="mt-8"> <div class="mt-8">
<button <button
@click="fromMnemonic()" @click="fromMnemonic()"
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-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
> >
Import Import
</button> </button>
<button <button
@click="onCancelClick()" @click="onCancelClick()"
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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
Cancel Cancel
</button> </button>
@@ -75,20 +75,15 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { import {
DEFAULT_ROOT_DERIVATION_PATH, DEFAULT_ROOT_DERIVATION_PATH,
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
} from "../libs/crypto"; } from "@/libs/crypto";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: {}, components: {},
@@ -96,7 +91,7 @@ interface Notification {
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
mnemonic = ""; mnemonic = "";
address = ""; address = "";

View File

@@ -51,14 +51,14 @@
<div class="mt-8"> <div class="mt-8">
<button <button
@click="incrementDerivation()" @click="incrementDerivation()"
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-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
> >
Increment and Import Increment and Import
</button> </button>
<button <button
@click="onCancelClick()" @click="onCancelClick()"
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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
> >
Cancel Cancel
</button> </button>

View File

@@ -24,7 +24,7 @@
<div class="mt-8"> <div class="mt-8">
<button <button
type="button" type="button"
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@click="onClickSaveChanges()" @click="onClickSaveChanges()"
> >
Save Changes Save Changes
@@ -32,7 +32,7 @@
<!-- SHOW ME instead while processing saving changes --> <!-- SHOW ME instead while processing saving changes -->
<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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="onClickCancel()" @click="onClickCancel()"
> >
Cancel Cancel

View File

@@ -106,7 +106,7 @@
<div class="mt-8"> <div class="mt-8">
<button <button
:disabled="isHiddenSave" :disabled="isHiddenSave"
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-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2"
@click="onSaveProjectClick()" @click="onSaveProjectClick()"
> >
<!-- SHOW if in idle state --> <!-- SHOW if in idle state -->
@@ -121,7 +121,7 @@
</button> </button>
<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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="onCancelClick()" @click="onCancelClick()"
> >
Cancel Cancel
@@ -138,6 +138,7 @@ import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
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";
@@ -145,18 +146,11 @@ import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { PlanVerifiableCredential } from "@/libs/endorserServer"; import { PlanVerifiableCredential } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { LMap, LMarker, LTileLayer, QuickNav }, components: { LMap, LMarker, LTileLayer, QuickNav },
}) })
export default class NewEditProjectView extends Vue { export default class NewEditProjectView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
agentDid = ""; agentDid = "";

View File

@@ -105,7 +105,7 @@
<button <button
v-if="activeDid === issuer || activeDid === agentDid" v-if="activeDid === issuer || activeDid === agentDid"
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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="onEditClick()" @click="onEditClick()"
> >
Edit Edit
@@ -116,7 +116,7 @@
<div class="text-center"> <div class="text-center">
<button <button
@click="openOfferDialog()" @click="openOfferDialog()"
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md" class="block w-full text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
> >
Offer (maybe with conditions)... Offer (maybe with conditions)...
</button> </button>
@@ -176,7 +176,7 @@
<a <a
v-if="allContacts.length >= 7" v-if="allContacts.length >= 7"
@click="onClickAllContactsGifting()" @click="onClickAllContactsGifting()"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md" class="block text-center text-md font-bold uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
> >
Show More Contacts&hellip; Show More Contacts&hellip;
</a> </a>
@@ -348,7 +348,12 @@ import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@@ -362,17 +367,6 @@ import {
PlanServerRecord, PlanServerRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import { Account } from "@/db/tables/accounts";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: {
@@ -385,7 +379,7 @@ interface Notification {
}, },
}) })
export default class ProjectViewView extends Vue { export default class ProjectViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
agentDid = ""; agentDid = "";
@@ -443,15 +437,6 @@ export default class ProjectViewView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) {
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);
const route = { const route = {
@@ -806,10 +791,7 @@ export default class ProjectViewView extends Vue {
), ),
), ),
); );
const confirmationClaim: serverUtil.GenericVerifiableCredential & { const confirmationClaim: serverUtil.GenericVerifiableCredential = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any;
} = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,

View File

@@ -213,6 +213,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
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";
@@ -225,18 +226,11 @@ import TopMessage from "@/components/TopMessage.vue";
import { OfferServerRecord, PlanData } from "@/libs/endorserServer"; import { OfferServerRecord, PlanData } from "@/libs/endorserServer";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage }, components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
apiServer = ""; apiServer = "";
projects: PlanData[] = []; projects: PlanData[] = [];

View File

@@ -0,0 +1,220 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4">
Beginning of BVC Saturday Meeting
</h1>
<div>
<h2 class="text-2xl m-2">You're Here</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="attended" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Attended</span>
</div>
<div class="m-2 flex">
<input type="checkbox" v-model="gaveTime" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Spent Time</span>
<span v-if="gaveTime">
<input
type="text"
placeholder="How much time"
v-model="hoursStr"
size="1"
class="border border-slate-400 h-6 px-2"
/>
hour(s)
</span>
<!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6" />
</div>
</div>
<div
v-if="attended || (gaveTime && hoursStr && hoursStr != '0')"
class="flex justify-center mt-4"
>
<button
@click="record()"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
>
Sign & Send
</button>
</div>
<div v-else class="flex justify-center mt-4">
<button
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
>
Select Your Actions
</button>
</div>
</section>
</template>
<script lang="ts">
import axios from "axios";
import { DateTime } from "luxon";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
bvcMeetingJoinClaim,
createAndSubmitClaim,
createAndSubmitGive,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
attended = true;
gaveTime = true;
hoursStr = "1";
todayOrPreviousStartDate = "";
async mounted() {
let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
if (currentOrPreviousSat.weekday < 6) {
// it's not Saturday or Sunday,
// so move back one week before setting to the Saturday
currentOrPreviousSat = currentOrPreviousSat.minus({ week: 1 });
}
const eventStartDateObj = currentOrPreviousSat
.set({ weekday: 6 })
.set({ hour: 9 })
.startOf("hour");
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
this.todayOrPreviousStartDate =
eventStartDateObj.toISO({
suppressMilliseconds: true,
}) || "";
}
async record() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer || "";
try {
const hoursNum = libsUtil.numberOrZero(this.hoursStr);
const identity = await libsUtil.getIdentity(activeDid);
// first send the claim for time given
let timeSuccess = false;
if (this.gaveTime && hoursNum > 0) {
const timeResult = await createAndSubmitGive(
axios,
apiServer,
identity,
activeDid,
undefined,
undefined,
hoursNum,
"HUR",
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
if (timeResult.type === "success") {
timeSuccess = true;
} else {
console.error("Error sending time:", timeResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
timeResult?.error?.userMessage ||
"There was an error sending the time.",
},
-1,
);
}
}
// now send the claim for attendance
let attendedSuccess = false;
if (this.attended) {
const attendResult = await createAndSubmitClaim(
bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate),
identity,
apiServer,
axios,
);
if (attendResult.type === "success") {
attendedSuccess = true;
} else {
console.error("Error sending attendance:", attendResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
attendResult?.error?.userMessage ||
"There was an error sending the attendance.",
},
-1,
);
}
}
if (timeSuccess || attendedSuccess) {
const actions =
timeSuccess && attendedSuccess
? "Your attendance and time have been recorded."
: timeSuccess
? "Your time has been recorded."
: "Your attendance has been recorded.";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: actions,
},
-1,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error sending claims.", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: error.userMessage || "There was an error sending claims.",
},
-1,
);
}
}
}
</script>

View File

@@ -0,0 +1,377 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4">
End of BVC Saturday Meeting
</h1>
<div>
<h2 class="text-2xl m-2">Confirm</h2>
<div v-if="loadingConfirms" class="flex justify-center">
<fa icon="spinner" class="animate-spin" />
</div>
<div v-else-if="claimsToConfirm.length === 0">
There are no claims yet today for you to confirm.
</div>
<ul class="border-t border-slate-300 m-2">
<li
class="border-b border-slate-300 py-2"
v-for="record in claimsToConfirm"
:key="record.id"
>
<div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start">
<span>
<input
type="checkbox"
:checked="claimsToConfirmSelected.includes(record.id)"
@click="
claimsToConfirmSelected.includes(record.id)
? claimsToConfirmSelected.splice(
claimsToConfirmSelected.indexOf(record.id),
1,
)
: claimsToConfirmSelected.push(record.id)
"
class="mr-2 h-6 w-6"
/>
</span>
{{
claimSpecialDescription(
record,
activeDid,
allMyDids,
allContacts,
)
}}
<a @click="onClickLoadClaim(record.id)">
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
/>
</a>
</span>
</div>
</li>
</ul>
</div>
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
<span>
{{
claimCountWithHidden === 1
? "There is 1 other claim with hidden details,"
: `There are ${claimCountWithHidden} other claims with hidden details,`
}}
so if you expected but do not see details from someone then ask them to
check that their activity is visible to you on their Contacts
<fa icon="users" class="text-slate-500" />
page.
</span>
</div>
<div>
<h2 class="text-2xl m-2">Anything else?</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="someoneGave" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Someone else gave</span>
<span v-if="someoneGave">
<input
type="text"
v-model="description"
size="20"
class="border border-slate-400 h-6 px-2"
/>
<br />
(Everyone likes personalized messages! 😁)
</span>
<!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6">...</span>
</div>
</div>
<div
v-if="claimsToConfirmSelected.length || (someoneGave && description)"
class="flex justify-center mt-4"
>
<button
@click="record()"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
>
Sign & Send
</button>
</div>
<div v-else class="flex justify-center mt-4">
<button
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
>
Choose What To Confirm
</button>
</div>
</section>
</template>
<script lang="ts">
import axios from "axios";
import { DateTime } from "luxon";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription,
containsHiddenDid,
createAndSubmitConfirmation,
createAndSubmitGive,
ErrorResult,
GenericServerRecord,
GenericVerifiableCredential,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@Component({
methods: { claimSpecialDescription },
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
claimCountWithHidden = 0;
claimsToConfirm: GenericServerRecord[] = [];
claimsToConfirmSelected: string[] = [];
description = "breakfast";
loadingConfirms = true;
someoneGave = false;
async created() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
}
async mounted() {
this.loadingConfirms = true;
let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
if (currentOrPreviousSat.weekday < 6) {
// it's not Saturday or Sunday,
// so move back one week before setting to the Saturday
currentOrPreviousSat = currentOrPreviousSat.minus({ week: 1 });
}
const eventStartDateObj = currentOrPreviousSat
.set({ weekday: 6 })
.set({ hour: 9 })
.startOf("hour");
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
const todayOrPreviousStartDate =
eventStartDateObj.toISO({
suppressMilliseconds: true,
}) || "";
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
const account: Account | undefined = await accountsDB.accounts
.where("did")
.equals(this.activeDid)
.first();
const identity: IIdentifier = JSON.parse(
(account?.identity as string) || "null",
);
const headers = {
Authorization: "Bearer " + (await accessToken(identity)),
};
try {
const response = await fetch(
this.apiServer +
"/api/claim/?" +
"issuedAt_greaterThanOrEqualTo=" +
encodeURIComponent(todayOrPreviousStartDate) +
"&excludeConfirmations=true",
{ headers },
);
if (!response.ok) {
console.log("Bad response", response);
throw new Error("Bad response when retrieving claims.");
}
await response.json().then((data) => {
const dataByOthers = R.reject(
(claim: GenericServerRecord) => claim.issuer === this.activeDid,
data,
);
const dataByOthersWithoutHidden = R.reject(
containsHiddenDid,
dataByOthers,
);
this.claimsToConfirm = dataByOthersWithoutHidden;
this.claimCountWithHidden =
dataByOthers.length - dataByOthersWithoutHidden.length;
});
} catch (error) {
console.error("Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error retrieving today's claims to confirm.",
},
-1,
);
}
this.loadingConfirms = false;
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
this.$router.push(route);
}
async record() {
try {
const identity = await libsUtil.getIdentity(this.activeDid);
// in parallel, make a confirmation for each selected claim and send them all to the server
const confirmResults = await Promise.allSettled(
this.claimsToConfirmSelected.map(async (jwtId) => {
const record = this.claimsToConfirm.find(
(claim) => claim.id === jwtId,
);
if (!record) {
return { type: "error", error: "Record not found." };
}
const identity = await libsUtil.getIdentity(this.activeDid);
return createAndSubmitConfirmation(
identity,
record.claim as GenericVerifiableCredential,
record.id,
record.handleId,
this.apiServer,
axios,
);
}),
);
// check for any rejected confirmations
const confirmsSucceeded = confirmResults.filter(
(result) =>
result.status === "fulfilled" && result.value.type === "success",
);
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
console.error("Error sending confirmations:", confirmResults);
const howMany = confirmsSucceeded.length === 0 ? "all" : "some";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was an error sending ${howMany} of the confirmations.`,
},
-1,
);
}
// now send the give for the description
let giveSucceeded = false;
if (this.someoneGave) {
const giveResult = await createAndSubmitGive(
axios,
this.apiServer,
identity,
undefined,
this.activeDid,
this.description,
undefined,
undefined,
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
giveSucceeded = giveResult.type === "success";
if (!giveSucceeded) {
console.error("Error sending give:", giveResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
(giveResult as ErrorResult)?.error?.userMessage ||
"There was an error sending that give.",
},
-1,
);
}
}
if (confirmsSucceeded.length > 0 || giveSucceeded) {
const confirms =
confirmsSucceeded.length === 1 ? "confirmation" : "confirmations";
const actions =
confirmsSucceeded.length > 0 && giveSucceeded
? `Your ${confirms} and that give have been recorded.`
: giveSucceeded
? "That give has been recorded."
: "Your " +
confirms +
" " +
(confirmsSucceeded.length === 1 ? "has" : "have") +
" been recorded.";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: actions,
},
-1,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error sending claims.", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: error.userMessage || "There was an error sending claims.",
},
-1,
);
}
}
}
</script>

View File

@@ -0,0 +1,52 @@
<template>
<QuickNav />
<TopMessage />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4">
Bountiful Voluntaryist Community Actions
</h1>
<div>
<router-link
:to="{ name: 'quick-action-bvc-begin' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Beginning of Meeting
</router-link>
<router-link
:to="{ name: 'quick-action-bvc-end' }"
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
End of Meeting
</router-link>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcView extends Vue {}
</script>

View File

@@ -105,21 +105,15 @@ import {
LTileLayer, LTileLayer,
} from "@vue-leaflet/vue-leaflet"; } from "@vue-leaflet/vue-leaflet";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue";
const DEFAULT_LAT_LONG_DIFF = 0.01; const DEFAULT_LAT_LONG_DIFF = 0.01;
const WORLD_ZOOM = 2; const WORLD_ZOOM = 2;
const DEFAULT_ZOOM = 2; const DEFAULT_ZOOM = 2;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: {
QuickNav, QuickNav,
@@ -130,7 +124,7 @@ interface Notification {
}, },
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
isChoosingSearchBox = false; isChoosingSearchBox = false;
isNewMarkerSet = false; isNewMarkerSet = false;

View File

@@ -1,5 +1,5 @@
<template> <template>
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile" />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <!-- Back -->
@@ -22,7 +22,7 @@
<span> <span>
<router-link <router-link
:to="{ name: 'help' }" :to="{ name: 'help' }"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1" class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
> >
Help Help
</router-link> </router-link>
@@ -48,7 +48,7 @@
<div class="bg-slate-100 rounded-md overflow-hidden p-4 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden p-4 mb-4">
<button <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-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="showSeedPhrase" @click="showSeedPhrase"
> >
Reveal my Seed Phrase Reveal my Seed Phrase
@@ -65,25 +65,20 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db/index";
import * as R from "ramda"; import * as R from "ramda";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
interface Account { interface Account {
mnemonic: string; mnemonic: string;
} }
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class SeedBackupView extends Vue { export default class SeedBackupView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeAccount: Account | null | undefined = null; activeAccount: Account | null | undefined = null;
numAccounts = 0; numAccounts = 0;

View File

@@ -36,20 +36,20 @@
</p> </p>
<a <a
@click="onClickYes()" @click="onClickYes()"
class="block w-full text-center text-lg uppercase bg-blue-600 text-white px-2 py-3 rounded-md" class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
> >
Yes, generate one Yes, generate one
</a> </a>
<a <a
@click="onClickNo()" @click="onClickNo()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-2"
> >
No, I have a seed No, I have a seed
</a> </a>
<a <a
v-if="numAccounts > 0" v-if="numAccounts > 0"
@click="onClickDerive()" @click="onClickDerive()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-2"
> >
Derive new address from existing seed Derive new address from existing seed
</a> </a>

View File

@@ -53,8 +53,10 @@
<script lang="ts"> <script lang="ts">
import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js"; import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { World } from "@/components/World/World.js"; import { World } from "@/components/World/World.js";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app";
interface RendererSVGType { interface RendererSVGType {
domElement: Element; domElement: Element;
@@ -64,16 +66,9 @@ interface Dictionary<T> {
[key: string]: T; [key: string]: T;
} }
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { World, QuickNav } }) @Component({ components: { World, QuickNav } })
export default class StatisticsView extends Vue { export default class StatisticsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
world: World; world: World;
worldProperties: Dictionary<number> = {}; worldProperties: Dictionary<number> = {};

View File

@@ -3,6 +3,8 @@ const { gitDescribeSync } = require("git-describe");
const { exec } = require("child_process"); const { exec } = require("child_process");
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash; process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
const TIME_SAFARI_APP_TITLE =
process.env.TIME_SAFARI_APP_TITLE || require("./package.json").name;
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true, transpileDependencies: true,
@@ -30,6 +32,7 @@ module.exports = defineConfig({
], ],
}, },
pwa: { pwa: {
name: TIME_SAFARI_APP_TITLE,
iconPaths: { iconPaths: {
faviconSVG: "img/icons/safari-pinned-tab.svg", faviconSVG: "img/icons/safari-pinned-tab.svg",
}, },