Compare commits

...

51 Commits

Author SHA1 Message Date
Trent Larson 1459719a47 remove a lingering debug console.log 8 months ago
Trent Larson 5994365a6c fix title of the test app 8 months ago
Trent Larson 28f72640d7 add linting before any build 8 months ago
Trent Larson ab81648aca fix linting 8 months ago
Trent Larson 5828a290c7 Merge remote-tracking branch 'original-origin/master' into feat/vitejs-trent 8 months ago
Trent Larson 78fab735e6 avoid a huge error message in a likely-well-known scenario 8 months ago
Trent Larson 2ae165d56f reorder home page vapid check to avoid an error on localhost 8 months ago
Trent Larson 0fbd1ad51a add missing Dexie import (which causes failure upon download click) 8 months ago
Trent Larson d49bf61524 on home page, change the filtered button color 8 months ago
trentlarson 1a80bbb714 Merge pull request 'ui-additions-2024-03' (#113) from ui-additions-2024-03 into master 8 months ago
Trent Larson 5a2a8659f7 Merge branch 'master' into ui-additions-2024-03 8 months ago
Trent Larson 0632fb9b39 show in description when recipient is a project (not just Anonymous) 8 months ago
Trent Larson 640d273646 filter by selections (now all working), add cache for plans 8 months ago
trentlarson 8f4289c14d Merge pull request 'send a time for notifications to the push server' (#112) from notify-time into master 8 months ago
trentlarson 7f56c90d97 Merge pull request 'ui-fixes-2024-03' (#111) from ui-fixes-2024-03 into master 8 months ago
Trent Larson 8e1daf7015 feed filter: save the changed values to the DB, go to map if no location chosen, reload if necessary 8 months ago
Jose Olarte III e90a0be6d9 Names and variables for filter toggles 8 months ago
Trent Larson 489bb76a60 adjust more code to the PushSubscriptionJSON 8 months ago
Trent Larson 2ca33bb9eb adjust the notification-subscription objects to try and send correct info 8 months ago
Trent Larson 121181c6a1 add adjustment to UTC hour for notification time 8 months ago
Trent Larson e4cf79b558 update tasks 8 months ago
Trent Larson 7692cc2b35 add logic to send a time for notifications 8 months ago
Jose Olarte III 0b4f2484f7 Additions to Account View 8 months ago
Jose Olarte III cfd53bc186 Removed one more 8 months ago
Jose Olarte III 7f66addfe3 Filter options reduced for release 8 months ago
Jose Olarte III 4635c1ac48 Feed filters dialog 8 months ago
Jose Olarte III 55da3d0b1c Map fix #2 8 months ago
Jose Olarte III dce4d3cc72 Button width changes 8 months ago
Jose Olarte III e028197a2a Optimized grid space for wider screens 8 months ago
Jose Olarte III 0dab475d8b Fixed map z-index 8 months ago
Jose Olarte III 4e227fc07a Added close icon to gifted prompts dialog 8 months ago
Trent Larson 2dfc8fedaa refactor tasks 8 months ago
Jason Buchanan 035f2a5b04
docs: adding do for updated development server run command 8 months ago
Jason Buchanan 09dccc34d6
fix: buffer typescript error in util.ts when parsing ArrayBuffer 8 months ago
Trent Larson b28104af5b bump version and add -beta 8 months ago
Trent Larson 3a07e31d63 bump to version 0.3.6 8 months ago
Trent Larson 35455e6648 fix check for more camera-device options 8 months ago
Trent Larson 0e2c5af16e add onboarding help instructions as separate page 8 months ago
trentlarson 1fc5b0ea2b Merge pull request 'add button during photo to switch to mirror mode' (#109) from photo-reverse into master 8 months ago
Jason Buchanan ca240ab795
fix: es modules syntax for buffer deps instead of commonjs require 8 months ago
Jason Buchanan 01b5ca6ec8
chore: update vitejs config to deploy on the same default port as the @vue/cli-service 8 months ago
Jason Buchanan 6f49260c1e
fix: AccountViewView.vue template not resolving dep for dexie-export-import/dist/import 8 months ago
Jason Buchanan 38f44771e9
Initial stab at vitejs update 8 months ago
Trent Larson be2154f847 change icon for detail view (from circle-info to file-lines) 8 months ago
Trent Larson 4ad4cab25e add blurb explaining what data is shared with the world 8 months ago
Trent Larson e020caaa50 show warnings before dismissing prompt, and add to tasks and help 8 months ago
Trent Larson 40d12b1f9c add button on photo to switch to mirror mode 8 months ago
Trent Larson 28754bdfb1 bump version to 0.3.5 8 months ago
Trent Larson 2b8f9579f1 fix so that project agent & location removals get saved 8 months ago
Trent Larson 6dc0c2cd58 add a camera-switch button 8 months ago
trentlarson cc6d0958dc Merge pull request 'fix: npm audit fix to resolve vulnerabilities 1 low, 3 moderate, 1 high' (#108) from jsnbuchanan/crowd-funder-for-time-pwa:master into master 8 months ago
  1. 8
      .env.production
  2. 7
      .eslintrc.js
  3. 11
      CHANGELOG.md
  4. 11
      README.md
  5. 3
      babel.config.js
  6. 17
      index.html
  7. 26195
      package-lock.json
  8. 106
      package.json
  9. 39
      project.task.yaml
  10. 17
      public/index.html
  11. 168
      src/App.vue
  12. 3
      src/assets/styles/tailwind.css
  13. 219
      src/components/FeedFilters.vue
  14. 110
      src/components/GiftedDialog.vue
  15. 74
      src/components/GiftedPhotoDialog.vue
  16. 10
      src/components/GiftedPrompts.vue
  17. 26
      src/components/OfferDialog.vue
  18. 4
      src/constants/app.ts
  19. 8
      src/db/tables/settings.ts
  20. 2
      src/libs/crypto/index.ts
  21. 113
      src/libs/endorserServer.ts
  22. 54
      src/libs/util.ts
  23. 6
      src/main.ts
  24. 2
      src/registerServiceWorker.ts
  25. 140
      src/router/index.ts
  26. 1
      src/util.d.ts
  27. 48
      src/views/AccountViewView.vue
  28. 24
      src/views/ConfirmContactView.vue
  29. 10
      src/views/ContactAmountsView.vue
  30. 4
      src/views/ContactQRScanShowView.vue
  31. 24
      src/views/ContactScanView.vue
  32. 58
      src/views/ContactsView.vue
  33. 105
      src/views/GiftedDetails.vue
  34. 13
      src/views/HelpNotificationsView.vue
  35. 69
      src/views/HelpOnboardingView.vue
  36. 39
      src/views/HelpView.vue
  37. 200
      src/views/HomeView.vue
  38. 28
      src/views/ImportAccountView.vue
  39. 28
      src/views/ImportDerivedAccountView.vue
  40. 32
      src/views/NewEditAccountView.vue
  41. 53
      src/views/NewEditProjectView.vue
  42. 40
      src/views/ProjectViewView.vue
  43. 6
      src/views/ProjectsView.vue
  44. 8
      src/views/QuickActionBvcEndView.vue
  45. 8
      src/views/SearchAreaView.vue
  46. 64
      src/views/StartView.vue
  47. 74
      tsconfig.json
  48. 18
      vite.config.mjs
  49. 4
      vue.config.js

8
.env.production

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

7
.eslintrc.js

@ -2,6 +2,7 @@ module.exports = {
root: true, root: true,
env: { env: {
node: true, node: true,
es2022: true,
}, },
extends: [ extends: [
"plugin:vue/vue3-essential", "plugin:vue/vue3-essential",
@ -9,9 +10,9 @@ module.exports = {
"@vue/typescript/recommended", "@vue/typescript/recommended",
"plugin:prettier/recommended", "plugin:prettier/recommended",
], ],
parserOptions: { // parserOptions: {
ecmaVersion: 2020, // ecmaVersion: 2020,
}, // },
rules: { rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",

11
CHANGELOG.md

@ -10,7 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Nothing - Nothing
## [0.3.4] - 2024.03.21 ## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141
### Added
- Button to mirror photo during video
- More detailed onboarding help screen
- Public-data blurb
### Changed in DB or environment
- Nothing
## [0.3.5] - 2024.03.23 - 28754bdfb1e11aa221dd49a5dce4219b69cf6a9d
### Added ### Added
- Photo on gift records - Photo on gift records
### Fixed ### Fixed

11
README.md

@ -18,6 +18,11 @@ npm install
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
``` ```
npm run dev
```
### Builds the production app
```
npm run serve npm run serve
``` ```
@ -42,7 +47,7 @@ npm run lint
``` ```
# (See .env.development for more details.) # (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. # 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 VITE_TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build
``` ```
* Production * Production
@ -55,8 +60,6 @@ npm run build
* `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).
* 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. * 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) * [Tag with the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
@ -128,7 +131,7 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.) * Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.)
* Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".) * Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".)
* Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.) * Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals`; in Firefox, go to `about:serviceworkers`.)
* Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.) * Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
(If you find more, add them to the HelpNotificationsView.vue file.) (If you find more, add them to the HelpNotificationsView.vue file.)

3
babel.config.js

@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

17
index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>TimeSafari</title>
</head>
<body>
<noscript>
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

26195
package-lock.json

File diff suppressed because it is too large

106
package.json

@ -1,95 +1,91 @@
{ {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.4", "version": "0.3.7-beta",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build", "serve": "vite preview",
"lint": "vue-cli-service lint" "build": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && vite build",
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src"
}, },
"dependencies": { "dependencies": {
"@dicebear/collection": "^5.3.5", "@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.3.5", "@dicebear/core": "^5.4.1",
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.3", "@fortawesome/vue-fontawesome": "^3.0.6",
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.1.1",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@veramo/core": "^5.4.1", "@veramo/core": "^5.6.0",
"@veramo/credential-w3c": "^5.4.1", "@veramo/credential-w3c": "^5.6.0",
"@veramo/data-store": "^5.4.1", "@veramo/data-store": "^5.6.0",
"@veramo/did-manager": "^5.4.1", "@veramo/did-manager": "^5.6.0",
"@veramo/did-provider-ethr": "^5.4.1", "@veramo/did-provider-ethr": "^5.6.0",
"@veramo/did-resolver": "^5.4.1", "@veramo/did-resolver": "^5.6.0",
"@veramo/key-manager": "^5.4.1", "@veramo/key-manager": "^5.6.0",
"@vueuse/core": "^10.4.1", "@vueuse/core": "^10.9.0",
"@zxing/text-encoding": "^0.9.0", "@zxing/text-encoding": "^0.9.0",
"axios": "^1.5.0", "axios": "^1.6.8",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"core-js": "^3.32.1", "dexie": "^3.2.7",
"dexie": "^3.2.4", "dexie-export-import": "^4.1.1",
"dexie-export-import": "^4.0.7", "did-jwt": "^7.4.7",
"did-jwt": "^7.2.7", "ethereum-cryptography": "^2.1.3",
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.1.2", "ethr-did-resolver": "^8.1.2",
"git-describe": "^4.1.1", "git-describe": "^4.1.1",
"jdenticon": "^3.2.0", "jdenticon": "^3.2.0",
"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.7.0",
"lru-cache": "^10.2.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"moment": "^2.29.4", "moment": "^2.30.1",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pina": "^0.20.2204228", "pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.2.0", "pinia-plugin-persistedstate": "^3.2.1",
"qr-code-generator-vue3": "^1.4.21", "qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.0", "ramda": "^0.29.1",
"readable-stream": "^4.4.2", "readable-stream": "^4.5.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.14",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"simple-vue-camera": "^1.1.3", "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",
"vue": "^3.3.4", "vue": "^3.4.21",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.2", "vue-facing-decorator": "^3.0.4",
"vue-qrcode-reader": "^5.4.1", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.2.4", "vue-router": "^4.3.0",
"web-did-resolver": "^2.0.27" "web-did-resolver": "^2.0.27"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.4", "@types/leaflet": "^1.9.8",
"@types/ramda": "^0.29.3", "@types/ramda": "^0.29.11",
"@types/three": "^0.155.1", "@types/three": "^0.155.1",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.6.0", "@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.8",
"@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.19",
"eslint": "^8.53.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.23.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"postcss": "^8.4.29", "postcss": "^8.4.38",
"prettier": "^3.1.0", "prettier": "^3.2.5",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.4.1",
"typescript": "~5.2.2" "typescript": "~5.2.2",
"vite": "^5.2.0"
} }
} }

39
project.task.yaml

@ -1,35 +1,37 @@
tasks : tasks :
- bug - landscape doesn't show full camera - fix the notification link to the app
- bug - got blank screen and errors on iPhone with no bottom tabs - 01 change scanning flow - allow them to stay on the QR/scanning screen after scanning someone
- 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
- .2 when adding a claim on home screen, push that claim to the top of the list
- 24 allow a person record with interests, including location; purpose? contact methods? enhance other connections the same? (suggestion from Philippines) assignee-group:ui - 24 contextual tutorials https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
- 24 Move to Vite assignee:jason
- feeds - add "remote" filter, if they choose 'visible' then warn that they won't see any others, cache list & don't reload front page on change
- .1 add shortcut from project (etc?) to the public project page in a browser
- .1 add KindSpring link to ideas
- .1 on feed, don't show "to someone anonymous" if it's to a project - .1 on feed, don't show "to someone anonymous" if it's to a project
- .1 on ideas, put an "x" to close it assignee-group:ui
- 16 save data backups in Google - 16 save data backups in Google
- 16 generate and use passkeys for identities - 16 generate and use passkeys for identities
- .5 show "give" buttons (eg. from anonymous) even if they can't give, greyed out, and give them a warning and instructions
- .2 when adding a claim on home screen, push that claim to the top of the list
- .2 fix give dialog from "more contacts" off home page to allow giving to this user - .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 assignee-group:ui
- .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) - .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 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
- .2 list the "show more" contacts alphabetically - .2 list the "show more" contacts alphabetically
- .5 add back the explicit wait for browser subscription timing problems?
- .5 make Time Safari a share_target for images - .5 make Time Safari a share_target for images
- 08 add image on profile - 08 add image on profile
- ask to detect location & record it in settings - 01 ask to detect location & record it in settings
- if personal location is set, show potential local affiliations - 01 if personal location is set, show potential local affiliations
- 02 refactor the buttons for chosing a search location so that the actions are clear assignee-group:ui
- 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
@ -66,6 +68,7 @@ tasks :
- .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 (feed, project list, give & offer lists) - .2 record when InfiniteScroll hits the end of the list and don't trigger any more loads (feed, project list, give & offer lists)
- bug (that is hard to reproduce) - got blank screen and errors on iPhone with no bottom tabs
- 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)
- the confirm button on each give on the ProjectViewView page doesn't have all the context of the ClaimView page, so it can show sometimes inappropriately; consider consolidation - the confirm button on each give on the ProjectViewView page doesn't have all the context of the ClaimView page, so it can show sometimes inappropriately; consider consolidation
@ -97,7 +100,7 @@ tasks :
- .3 check that Android shows "back" buttons on screens without bottom tray - .3 check that Android shows "back" buttons on screens without bottom tray
- .1 Make give description text box into something that expands as they type? - .1 Make give description text box into something that expands as they type?
- .2 Show a warning if both giver and recipient are the same (but still allow?) - .2 Show a warning if both giver and recipient are the same (but still allow?)
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui - .5 Shrink the buttons on project pages so they don't expand to the width of the screen assignee-group:ui
- .5 Display a more appealing confirmation on the map when erasing the marker - .5 Display a more appealing confirmation on the map when erasing the marker
- .5 remove references to localStorage for projectId (now that it's pulling from the path) - .5 remove references to localStorage for projectId (now that it's pulling from the path)
- switch some checks for activeDid to check for isRegistered - switch some checks for activeDid to check for isRegistered
@ -124,11 +127,14 @@ tasks :
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie) - 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- .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 - 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 allow a person record with interests, including location; purpose? contact methods? enhance other connections the same? (suggestion from Philippines) assignee-group:ui
- 24 brief introduction slides https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
- 12 feedback https://docs.google.com/document/d/11C_K3RM0rgo0onih20KFhcIzukZyq_CRWqaWX5om_kM/edit#heading=h.iwiwcydou5hw
- 32 accept images for projects - 32 accept images for projects
- 32 accept images for contacts - 32 accept images for contacts
- import project interactions from GitHub/GitLab and manage signing - import project interactions from GitHub/GitLab and manage signing
@ -142,7 +148,6 @@ tasks :
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances - for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning) - for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
- .5 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
- .5 Replace Gifted/Give in ContactsView with GiftedDialog - .5 Replace Gifted/Give in ContactsView with GiftedDialog
- Stats : - Stats :

17
public/index.html

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

168
src/App.vue

@ -1,7 +1,7 @@
<template> <template>
<router-view /> <router-view />
<!-- https://github.com/emmanuelsw/notiwind --> <!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
<NotificationGroup group="alert"> <NotificationGroup group="alert">
<div <div
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end" class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
@ -129,6 +129,7 @@
</div> </div>
</NotificationGroup> </NotificationGroup>
<!-- These are general-purpose messages - except there are some for turning app notifications on and off. -->
<NotificationGroup group="modal"> <NotificationGroup group="modal">
<div class="fixed z-[100] top-0 inset-x-0 w-full"> <div class="fixed z-[100] top-0 inset-x-0 w-full">
<Notification <Notification
@ -148,6 +149,7 @@
class="w-full" class="w-full"
role="alert" role="alert"
> >
<!-- type "confirm" will post a message and, with onYes function, show a "Yes" button to call that function -->
<div <div
v-if="notification.type === 'confirm'" v-if="notification.type === 'confirm'"
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"
@ -161,6 +163,7 @@
</p> </p>
<button <button
v-if="notification.onYes"
@click=" @click="
notification.onYes(); notification.onYes();
close(notification.id); close(notification.id);
@ -174,7 +177,7 @@
@click="close(notification.id)" @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" class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
> >
Cancel {{ notification.onYes ? "Cancel" : "Close" }}
</button> </button>
</div> </div>
</div> </div>
@ -188,7 +191,7 @@
> >
<div class="w-full px-6 py-6 text-slate-900 text-center"> <div class="w-full px-6 py-6 text-slate-900 text-center">
<p v-if="serviceWorkerReady" class="text-lg mb-4"> <p v-if="serviceWorkerReady" class="text-lg mb-4">
Would you like to <b>turn on</b> notifications for this app? Would you like to be notified of new activity once a day?
</p> </p>
<p v-else class="text-lg mb-4"> <p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 10 Waiting for system initialization, which may take up to 10
@ -196,22 +199,42 @@
<fa icon="spinner" spin /> <fa icon="spinner" spin />
</p> </p>
<button <div v-if="serviceWorkerReady">
v-if="serviceWorkerReady" <span class="flex flex-row justify-center">
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" <span class="mt-2">Yes, tell me at: </span>
@click=" <input
close(notification.id); type="number"
turnOnNotifications(); class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
" v-model="hourInput"
> />
Turn on Notifications <span
</button> class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
@click="hourAm = !hourAm"
>
<span v-if="hourAm"> AM <fa icon="chevron-down" /> </span>
<span v-else> PM <fa icon="chevron-up" /> </span>
</span>
</span>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md"
@click="
() => {
if (checkHour()) {
close(notification.id);
turnOnNotifications();
}
}
"
>
Turn on Daily Message
</button>
</div>
<button <button
@click="close(notification.id)" @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" class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
> >
Maybe Later No, Not Now
</button> </button>
</div> </div>
</div> </div>
@ -294,8 +317,11 @@
<style></style> <style></style>
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import axios from "axios"; import axios from "axios";
import { Vue, Component } from "vue-facing-decorator";
import * as libsUtil from "@/libs/util";
interface ServiceWorkerMessage { interface ServiceWorkerMessage {
type: string; type: string;
data: string; data: string;
@ -319,6 +345,10 @@ interface VapidResponse {
}; };
} }
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
notifyTime: { utcHour: number };
}
import { DEFAULT_PUSH_SERVER, NotificationIface } 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";
@ -329,7 +359,9 @@ export default class App extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
b64 = ""; b64 = "";
serviceWorkerReady = false; hourAm = true;
hourInput = "8";
serviceWorkerReady = true;
async mounted() { async mounted() {
try { try {
@ -340,25 +372,29 @@ export default class App extends Vue {
pushUrl = settings.webPushServer; pushUrl = settings.webPushServer;
} }
await axios if (pushUrl.startsWith("http://localhost")) {
.get(pushUrl + "/web-push/vapid") console.log("Not checking for VAPID in this local environment.");
.then((response: VapidResponse) => { } else {
this.b64 = response.data?.vapidKey || ""; await axios
console.log("Got vapid key:", this.b64); .get(pushUrl + "/web-push/vapid")
navigator.serviceWorker.addEventListener("controllerchange", () => { .then((response: VapidResponse) => {
console.log("New service worker is now controlling the page"); this.b64 = response.data?.vapidKey || "";
console.log("Got vapid key:", this.b64);
navigator.serviceWorker.addEventListener("controllerchange", () => {
console.log("New service worker is now controlling the page");
});
}); });
}); if (!this.b64) {
if (!this.b64) { this.$notify(
this.$notify( {
{ group: "alert",
group: "alert", type: "danger",
type: "danger", title: "Error Setting Notifications",
title: "Error Setting Notifications", text: "Could not set notifications.",
text: "Could not set notifications.", },
}, -1,
-1, );
); }
} }
} catch (error) { } catch (error) {
if (window.location.host.startsWith("localhost")) { if (window.location.host.startsWith("localhost")) {
@ -458,6 +494,48 @@ export default class App extends Vue {
}); });
} }
// this allows us to show an error without closing the dialog
checkHour() {
if (!libsUtil.isNumeric(this.hourInput)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Number",
text: "The time must be an hour number.",
},
5000,
);
return false;
}
const hourNum = libsUtil.numberOrZero(this.hourInput);
if (!Number.isInteger(hourNum)) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Whole Number",
text: "The time must be a whole hour number.",
},
5000,
);
return false;
}
if (hourNum < 1 || 12 < hourNum) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not a Whole Number",
text: "The time must be an hour between 1 and 12.",
},
5000,
);
return false;
}
return true;
}
public async turnOnNotifications() { public async turnOnNotifications() {
return this.askPermission() return this.askPermission()
.then((permission) => { .then((permission) => {
@ -483,13 +561,25 @@ export default class App extends Vue {
}, },
-1, -1,
); );
this.sendSubscriptionToServer(subscription); // we already checked that this is a valid hour number
return subscription; const rawHourNum = libsUtil.numberOrZero(this.hourInput);
const adjHourNum = rawHourNum + (this.hourAm ? 0 : 12);
const hourNum = adjHourNum % 24;
const utcHour =
hourNum + Math.round(new Date().getTimezoneOffset() / 60);
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
const subscriptionWithTime: PushSubscriptionWithTime = {
notifyTime: { utcHour: finalUtcHour },
...subscription.toJSON(),
};
await this.sendSubscriptionToServer(subscriptionWithTime);
return subscriptionWithTime;
} else { } else {
throw new Error("Subscription object is not available."); throw new Error("Subscription object is not available.");
} }
}) })
.then(async (subscription) => { .then(async (subscription: PushSubscriptionWithTime) => {
console.log( console.log(
"Subscription data sent to server and all finished successfully.", "Subscription data sent to server and all finished successfully.",
); );
@ -580,7 +670,7 @@ export default class App extends Vue {
} }
private sendSubscriptionToServer( private sendSubscriptionToServer(
subscription: PushSubscription, subscription: PushSubscriptionWithTime,
): Promise<void> { ): Promise<void> {
console.log("About to send subscription...", subscription); console.log("About to send subscription...", subscription);
return fetch("/web-push/subscribe", { return fetch("/web-push/subscribe", {

3
src/assets/styles/tailwind.css

@ -1,9 +1,8 @@
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
@layer base { @layer base {
html { html {
font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important; font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important;

219
src/components/FeedFilters.vue

@ -0,0 +1,219 @@
<template>
<div v-if="visible" id="dialogFeedFilters" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Feed Filters</h1>
<p class="mb-4 font-bold">Show only activities that</p>
<div class="grid grid-cols-1 gap-2">
<div
class="flex items-center justify-between cursor-pointer"
@click="toggleHasVisibleDid()"
>
<!-- label -->
<div>Include someone visible to me</div>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="hasVisibleDid"
name="toggleFilterFromMyContacts"
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>
</div>
<em>or</em>
<div
class="flex items-center justify-between cursor-pointer"
@click="
hasSearchBox
? toggleNearby()
: $router.push({ name: 'search-area' })
"
>
<!-- label -->
<div>Are nearby</div>
<!-- toggle -->
<div v-if="hasSearchBox" class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="isNearby"
name="toggleFilterNearby"
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>
<div v-else class="relative ml-2">
<button class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500">
Select Location
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-4">
<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="setAll()"
>
Set All
</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="clearAll()"
>
Clear All
</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="done()"
>
Done
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import {
LMap,
LMarker,
LRectangle,
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { db } from "@/db/index";
@Component({
components: {
LRectangle,
LMap,
LMarker,
LTileLayer,
},
})
export default class FeedFilters extends Vue {
onCloseIfChanged = () => {};
hasSearchBox = false;
hasVisibleDid = false;
isNearby = false;
settingChanged = false;
visible = false;
async open(onCloseIfChanged: () => void) {
this.onCloseIfChanged = onCloseIfChanged;
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.hasVisibleDid = !!settings?.filterFeedByVisible;
this.isNearby = !!settings?.filterFeedByNearby;
if (settings?.searchBoxes && settings.searchBoxes.length > 0) {
this.hasSearchBox = true;
}
this.settingChanged = false;
this.visible = true;
}
toggleHasVisibleDid() {
this.settingChanged = true;
this.hasVisibleDid = !this.hasVisibleDid;
db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid,
});
}
toggleNearby() {
this.settingChanged = true;
this.isNearby = !this.isNearby;
db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: this.isNearby,
});
}
async clearAll() {
if (this.hasVisibleDid || this.isNearby) {
this.settingChanged = true;
}
db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: false,
filterFeedByVisible: false,
});
this.hasVisibleDid = false;
this.isNearby = false;
}
async setAll() {
if (!this.hasVisibleDid || !this.isNearby) {
this.settingChanged = true;
}
db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: true,
filterFeedByVisible: true,
});
this.hasVisibleDid = true;
this.isNearby = true;
}
close() {
if (this.settingChanged) {
this.onCloseIfChanged();
}
this.visible = false;
}
done() {
this.close();
}
}
</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;
}
#dialogFeedFilters.dialog-overlay {
z-index: 99999;
overflow: scroll;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
</style>

110
src/components/GiftedDialog.vue

@ -53,25 +53,32 @@
}" }"
class="text-blue-500" class="text-blue-500"
> >
More Options Photo, ...
</router-link> </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
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p> </p>
<button <div class="grid grid-cols-1 sm:grid-cols-2 gap-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" <button
@click="confirm" 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"
> @click="confirm"
Sign &amp; Send >
</button> 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" <button
@click="cancel" 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> Cancel
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -195,6 +202,45 @@ export default class GiftedDialog extends Vue {
} }
async confirm() { async confirm() {
if (!this.activeDid) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You must select an identifier before you can record a give.",
},
3000,
);
return;
}
if (parseFloat(this.amountInput) < 0) {
this.$notify(
{
group: "alert",
type: "danger",
text: "You may not send a negative number.",
title: "",
},
2000,
);
return;
}
if (!this.description && !parseFloat(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]
}.`,
},
2000,
);
return;
}
this.close(); this.close();
this.$notify( this.$notify(
{ {
@ -229,32 +275,6 @@ export default class GiftedDialog extends Vue {
amountInput: number, amountInput: number,
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
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 (!description && !amountInput) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
},
-1,
);
return;
}
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
@ -339,6 +359,18 @@ export default class GiftedDialog extends Vue {
result.response?.data?.error?.message result.response?.data?.error?.message
); );
} }
explainData() {
this.$notify(
{
group: "alert",
type: "success",
title: "Data Sharing",
text: libsUtil.PRIVACY_MESSAGE,
},
-1,
);
}
} }
</script> </script>

74
src/components/GiftedPhotoDialog.vue

@ -43,22 +43,45 @@
<img :src="URL.createObjectURL(blob)" class="mt-2 rounded" /> <img :src="URL.createObjectURL(blob)" class="mt-2 rounded" />
</div> </div>
</div> </div>
<div v-else> <div v-else ref="cameraContainer">
<!-- <!--
Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically: 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 }" :resolution="{ width: 375, height: 812 }"
--> -->
<camera facingMode="environment" autoplay ref="camera"> <camera
facingMode="environment"
autoplay
ref="camera"
@started="cameraStarted()"
>
<div <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" class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
> >
<button <button
@click="takeImage" @click="takeImage()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none" 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> <fa icon="camera" class="w-[1em]"></fa>
</button> </button>
</div> </div>
<div
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
>
<button
@click="swapMirrorClass()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="left-right" class="w-[1em]"></fa>
</button>
</div>
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
<button
@click="switchCamera()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<fa icon="rotate" class="w-[1em]"></fa>
</button>
</div>
</camera> </camera>
</div> </div>
</div> </div>
@ -80,12 +103,12 @@ import { accessToken } from "@/libs/crypto";
export default class GiftedPhotoDialog extends Vue { export default class GiftedPhotoDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
activeDeviceNumber = 0;
activeDid = ""; activeDid = "";
blob: Blob | null = null; blob: Blob | null = null;
mirror = false;
numDevices = 0;
setImage: (arg: string) => void = () => {}; setImage: (arg: string) => void = () => {};
imageHeight?: number = window.innerHeight / 2;
imageWidth?: number = window.innerWidth / 2;
imageWarning = ".";
uploading = false; uploading = false;
visible = false; visible = false;
@ -129,6 +152,21 @@ export default class GiftedPhotoDialog extends Vue {
this.blob = null; this.blob = null;
} }
async cameraStarted() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
if (cameraComponent) {
this.numDevices = (await cameraComponent.devices(["videoinput"])).length;
this.mirror = cameraComponent.facingMode === "user";
}
}
async switchCamera() {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
this.activeDeviceNumber = (this.activeDeviceNumber + 1) % this.numDevices;
const devices = await cameraComponent?.devices(["videoinput"]);
cameraComponent?.changeCamera(devices[this.activeDeviceNumber].deviceId);
}
async takeImage(/* payload: MouseEvent */) { async takeImage(/* payload: MouseEvent */) {
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>; const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>;
@ -200,7 +238,6 @@ export default class GiftedPhotoDialog extends Vue {
<canvas id="canvas" width="320" height="240"></canvas> <canvas id="canvas" width="320" height="240"></canvas>
async cameraClicked() { async cameraClicked() {
console.log("camera_button clicked");
const video = document.querySelector("#video"); const video = document.querySelector("#video");
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
video: true, video: true,
@ -211,7 +248,6 @@ export default class GiftedPhotoDialog extends Vue {
} }
} }
photoSnapped() { photoSnapped() {
console.log("snap_photo clicked");
const video = document.querySelector("#video"); const video = document.querySelector("#video");
const canvas = document.querySelector("#canvas"); const canvas = document.querySelector("#canvas");
if ( if (
@ -232,7 +268,6 @@ export default class GiftedPhotoDialog extends Vue {
// data url of the image // data url of the image
const image_data_url = canvas?.toDataURL("image/jpeg"); const image_data_url = canvas?.toDataURL("image/jpeg");
console.log(image_data_url);
} }
} }
****/ ****/
@ -287,6 +322,17 @@ export default class GiftedPhotoDialog extends Vue {
this.blob = null; this.blob = null;
} }
} }
swapMirrorClass() {
this.mirror = !this.mirror;
if (this.mirror) {
(this.$refs.cameraContainer as HTMLElement).classList.add("mirror-video");
} else {
(this.$refs.cameraContainer as HTMLElement).classList.remove(
"mirror-video",
);
}
}
} }
</script> </script>
@ -311,4 +357,12 @@ export default class GiftedPhotoDialog extends Vue {
width: 100%; width: 100%;
max-width: 700px; max-width: 700px;
} }
.mirror-video {
transform: scaleX(-1);
-webkit-transform: scaleX(-1); /* For Safari */
-moz-transform: scaleX(-1); /* For Firefox */
-ms-transform: scaleX(-1); /* For IE */
-o-transform: scaleX(-1); /* For Opera */
}
</style> </style>

10
src/components/GiftedPrompts.vue

@ -1,7 +1,15 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Here's one:</h1> <h1 class="text-xl font-bold text-center mb-4 relative">
Here's one:
<div
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
@click="cancel"
>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</h1>
<span class="flex justify-between"> <span class="flex justify-between">
<span <span
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex" class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"

26
src/components/OfferDialog.vue

@ -50,18 +50,20 @@
<p class="text-center mt-6 mb-2 italic"> <p class="text-center mt-6 mb-2 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
</p> </p>
<button <div class="grid grid-cols-1 sm:grid-cols-2 gap-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" <button
@click="confirm" 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"
> @click="confirm"
Sign &amp; Send >
</button> 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" <button
@click="cancel" 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> Cancel
</button>
</div>
</div> </div>
</div> </div>
</template> </template>

4
src/constants/app.ts

@ -20,11 +20,11 @@ export enum AppString {
} }
export const DEFAULT_ENDORSER_API_SERVER = export const DEFAULT_ENDORSER_API_SERVER =
process.env.VUE_APP_DEFAULT_ENDORSER_API_SERVER || import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
AppString.TEST_ENDORSER_API_SERVER; AppString.TEST_ENDORSER_API_SERVER;
export const DEFAULT_IMAGE_API_SERVER = export const DEFAULT_IMAGE_API_SERVER =
process.env.VUE_APP_DEFAULT_IMAGE_API_SERVER || import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
AppString.TEST_IMAGE_API_SERVER; AppString.TEST_IMAGE_API_SERVER;
export const DEFAULT_PUSH_SERVER = export const DEFAULT_PUSH_SERVER =

8
src/db/tables/settings.ts

@ -16,6 +16,10 @@ export type Settings = {
activeDid?: string; // Active Decentralized ID activeDid?: string; // Active Decentralized ID
apiServer?: string; // API server URL apiServer?: string; // API server URL
filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
firstName?: string; // User's first name firstName?: string; // User's first name
isRegistered?: boolean; isRegistered?: boolean;
lastName?: string; // deprecated - put all names in firstName lastName?: string; // deprecated - put all names in firstName
@ -38,6 +42,10 @@ export type Settings = {
webPushServer?: string; // Web Push server URL webPushServer?: string; // Web Push server URL
}; };
export function isAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings.filterFeedByNearby || settings.filterFeedByVisible);
}
/** /**
* Schema for the Settings table in the database. * Schema for the Settings table in the database.
*/ */

2
src/libs/crypto/index.ts

@ -113,7 +113,7 @@ export const sign = async (privateKeyHex: string) => {
* The SimpleSigner returns a configured function for signing data. * The SimpleSigner returns a configured function for signing data.
* *
* @example * @example
* const signer = SimpleSigner(process.env.PRIVATE_KEY) * const signer = SimpleSigner(import.meta.env.PRIVATE_KEY)
* signer(data, (err, signature) => { * signer(data, (err, signature) => {
* ... * ...
* }) * })

113
src/libs/endorserServer.ts

@ -1,9 +1,11 @@
import { Axios, AxiosResponse, RawAxiosRequestHeaders } from "axios";
import * as didJwt from "did-jwt";
import { LRUCache } from "lru-cache";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { accessToken, SimpleSigner } from "@/libs/crypto";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
@ -49,7 +51,7 @@ export interface GenericVerifiableCredential {
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
} }
export interface GenericServerRecord extends GenericVerifiableCredential { export interface GenericCredWrapper extends GenericVerifiableCredential {
handleId?: string; handleId?: string;
id: string; id: string;
issuedAt: string; issuedAt: string;
@ -58,7 +60,7 @@ export interface GenericServerRecord extends GenericVerifiableCredential {
claim: Record<string, any>; claim: Record<string, any>;
claimType?: string; claimType?: string;
} }
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = { export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
"@type": "", "@type": "",
claim: {}, claim: {},
@ -68,7 +70,7 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
}; };
// a summary record; the VC is found the fullClaim field // a summary record; the VC is found the fullClaim field
export interface GiveServerRecord { export interface GiveSummaryRecord {
agentDid: string; agentDid: string;
amount: number; amount: number;
amountConfirmed: number; amountConfirmed: number;
@ -83,7 +85,7 @@ export interface GiveServerRecord {
} }
// a summary record; the VC is found the fullClaim field // a summary record; the VC is found the fullClaim field
export interface OfferServerRecord { export interface OfferSummaryRecord {
amount: number; amount: number;
amountGiven: number; amountGiven: number;
amountGivenConfirmed: number; amountGivenConfirmed: number;
@ -101,7 +103,7 @@ export interface OfferServerRecord {
} }
// a summary record; the VC is not currently part of this record // a summary record; the VC is not currently part of this record
export interface PlanServerRecord { export interface PlanSummaryRecord {
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;
@ -110,6 +112,7 @@ export interface PlanServerRecord {
issuerDid: string; issuerDid: string;
locLat?: number; locLat?: number;
locLon?: number; locLon?: number;
name?: string;
startTime?: string; startTime?: string;
url?: string; url?: string;
} }
@ -256,6 +259,10 @@ export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN"; const HIDDEN_DID = "did:none:HIDDEN";
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
export function isDid(did: string) { export function isDid(did: string) {
return did.startsWith("did:"); return did.startsWith("did:");
} }
@ -269,7 +276,7 @@ export function isEmptyOrHiddenDid(did?: string) {
} }
/** /**
* @return true for any nested string where func(input) === true * @return true for any string within this primitive/object/array where func(input) === true
* *
* Similar logic is found in endorser-mobile. * Similar logic is found in endorser-mobile.
*/ */
@ -304,6 +311,12 @@ export function containsHiddenDid(obj: any) {
return testRecursivelyOnStrings(isHiddenDid, obj); return testRecursivelyOnStrings(isHiddenDid, obj);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const containsNonHiddenDid = (obj: any) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return testRecursivelyOnStrings((s: any) => isDid(s) && !isHiddenDid(s), obj);
};
export function stripEndorserPrefix(claimId: string) { export function stripEndorserPrefix(claimId: string) {
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) { if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length); return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
@ -403,8 +416,11 @@ export function didInfoForContact(
return myId return myId
? { displayName: "You (Alt ID)", known: true } ? { displayName: "You (Alt ID)", known: true }
: isHiddenDid(did) : isHiddenDid(did)
? { displayName: "Someone Outside Your Network", known: false } ? { displayName: "Someone Totally Outside Your View", known: false }
: { displayName: "Someone Outside Contacts", known: false }; : {
displayName: "Someone Visible But Outside Your Contact List",
known: false,
};
} }
} }
@ -423,6 +439,71 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName; return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
} }
async function getHeaders(identity: IIdentifier | null) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
/**
* @param handleId nullable -- which means that "undefined" will be returned
* @param identity nullable -- which means no private info will be returned
* @param axios
* @param apiServer
*/
export async function getPlanFromCache(
handleId: string | null,
identity: IIdentifier | null,
axios: Axios,
apiServer: string,
) {
if (!handleId) {
return undefined;
}
let cred = planCache.get(handleId);
if (!cred) {
const url =
apiServer +
"/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId);
const headers = await getHeaders(identity);
try {
const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) {
cred = resp.data.data[0];
planCache.set(handleId, cred);
} else {
console.log(
"Failed to load plan with handle",
handleId,
" Got data:",
resp.data,
);
}
} catch (error) {
console.log(
"Failed to load plan with handle",
handleId,
" Got error:",
error,
);
}
}
return cred;
}
export async function setPlanInCache(
handleId: string,
planSummary: PlanSummaryRecord,
) {
planCache.set(handleId, planSummary);
}
/** /**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
* *
@ -475,7 +556,7 @@ export async function createAndSubmitGive(
vcClaim.image = imageUrl; vcClaim.image = imageUrl;
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericServerRecord, vcClaim as GenericCredWrapper,
identity, identity,
apiServer, apiServer,
axios, axios,
@ -524,7 +605,7 @@ export async function createAndSubmitOffer(
}; };
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericServerRecord, vcClaim as GenericCredWrapper,
identity, identity,
apiServer, apiServer,
axios, axios,
@ -695,7 +776,7 @@ const claimSummary = (claim: Record<string, any>) => {
similar code is also contained in endorser-mobile similar code is also contained in endorser-mobile
**/ **/
export const claimSpecialDescription = ( export const claimSpecialDescription = (
record: GenericServerRecord, record: GenericCredWrapper,
activeDid: string, activeDid: string,
identifiers: Array<string>, identifiers: Array<string>,
contacts: Array<Contact>, contacts: Array<Contact>,
@ -789,12 +870,12 @@ export const claimSpecialDescription = (
"...]" "...]"
); );
} else { } else {
return issuer + " declared " + claimSummary(claim as GenericServerRecord); return issuer + " declared " + claimSummary(claim as GenericCredWrapper);
} }
}; };
export const BVC_MEETUPS_PROJECT_CLAIM_ID = export const BVC_MEETUPS_PROJECT_CLAIM_ID =
process.env.VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID || import.meta.env.VITE_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 "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) => { export const bvcMeetingJoinClaim = (did: string, startTime: string) => {

54
src/libs/util.ts

@ -9,16 +9,11 @@ import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; 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 { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
// eslint-disable-next-line @typescript-eslint/no-var-requires export const PRIVACY_MESSAGE =
const Buffer = require("buffer/").Buffer; "The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow.";
// 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.
export const ONBOARD_MESSAGE =
"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> = {
@ -58,9 +53,13 @@ export function iconForUnitCode(unitCode: string) {
} }
// from https://stackoverflow.com/a/175787/845494 // from https://stackoverflow.com/a/175787/845494
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
// //
export function isNumeric(str: string): boolean { export function isNumeric(str: string): boolean {
return !isNaN(+str); // This ignore commentary is because typescript complains when you pass a string to isNaN.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return !isNaN(str) && !isNaN(parseFloat(str));
} }
export function numberOrZero(str: string): number { export function numberOrZero(str: string): number {
@ -71,7 +70,7 @@ 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+.-]+:/));
}; };
export const giveIsConfirmable = (veriClaim: GenericServerRecord) => { export const giveIsConfirmable = (veriClaim: GenericCredWrapper) => {
return veriClaim.claimType === "GiveAction"; return veriClaim.claimType === "GiveAction";
}; };
@ -87,7 +86,7 @@ export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
* @param veriClaim is expected to have fields: claim, claimType, and issuer * @param veriClaim is expected to have fields: claim, claimType, and issuer
*/ */
export const isGiveRecordTheUserCanConfirm = ( export const isGiveRecordTheUserCanConfirm = (
veriClaim: GenericServerRecord, veriClaim: GenericCredWrapper,
activeDid: string, activeDid: string,
confirmerIdList: string[] = [], confirmerIdList: string[] = [],
) => { ) => {
@ -103,9 +102,9 @@ export const isGiveRecordTheUserCanConfirm = (
* @returns the DID of the person who offered, or undefined if hidden * @returns the DID of the person who offered, or undefined if hidden
* @param veriClaim is expected to have fields: claim and issuer * @param veriClaim is expected to have fields: claim and issuer
*/ */
export const offerGiverDid: ( export const offerGiverDid: (arg0: GenericCredWrapper) => string | undefined = (
arg0: GenericServerRecord, veriClaim,
) => string | undefined = (veriClaim) => { ) => {
let giver; let giver;
if ( if (
veriClaim.claim.offeredBy?.identifier && veriClaim.claim.offeredBy?.identifier &&
@ -122,7 +121,7 @@ export const offerGiverDid: (
* @returns true if the user can fulfill the offer * @returns true if the user can fulfill the offer
* @param veriClaim is expected to have fields: claim, claimType, and issuer * @param veriClaim is expected to have fields: claim, claimType, and issuer
*/ */
export const canFulfillOffer = (veriClaim: GenericServerRecord) => { export const canFulfillOffer = (veriClaim: GenericCredWrapper) => {
return !!(veriClaim.claimType === "Offer" && offerGiverDid(veriClaim)); return !!(veriClaim.claimType === "Offer" && offerGiverDid(veriClaim));
}; };
@ -239,7 +238,7 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
}; };
export const sendTestThroughPushServer = async ( export const sendTestThroughPushServer = async (
subscription: PushSubscription, subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean, skipFilter: boolean,
): Promise<AxiosResponse> => { ): Promise<AxiosResponse> => {
await db.open(); await db.open();
@ -254,28 +253,11 @@ export const sendTestThroughPushServer = async (
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213 // Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION"; const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
const auth = Buffer.from(subscription.getKey("auth"));
const authB64 = auth
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const p256dh = Buffer.from(subscription.getKey("p256dh"));
const p256dhB64 = p256dh
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const newPayload = { const newPayload = {
endpoint: subscription.endpoint, // eslint-disable-next-line prettier/prettier
keys: { message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`,
auth: authB64,
p256dh: p256dhB64,
},
message: `Test, where you will see this message ${
skipFilter ? "un" : ""
}filtered.`,
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push", title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
...subscriptionJSON,
}; };
console.log("Sending a test web push message:", newPayload); console.log("Sending a test web push message:", newPayload);
const payloadStr = JSON.stringify(newPayload); const payloadStr = JSON.stringify(newPayload);

6
src/main.ts

@ -20,8 +20,10 @@ import {
faCalendar, faCalendar,
faCamera, faCamera,
faCheck, faCheck,
faChevronDown,
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faChevronUp,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
@ -45,6 +47,7 @@ import {
faHand, faHand,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faLeftRight,
faLocationDot, faLocationDot,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
@ -80,8 +83,10 @@ library.add(
faCalendar, faCalendar,
faCamera, faCamera,
faCheck, faCheck,
faChevronDown,
faChevronLeft, faChevronLeft,
faChevronRight, faChevronRight,
faChevronUp,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
@ -105,6 +110,7 @@ library.add(
faHand, faHand,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faLeftRight,
faLocationDot, faLocationDot,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,

2
src/registerServiceWorker.ts

@ -2,7 +2,7 @@
import { register } from "register-service-worker"; import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") { if (import.meta.env.NODE_ENV === "production") {
register("/sw_scripts-combined.js", { register("/sw_scripts-combined.js", {
ready() { ready() {
console.log( console.log(

140
src/router/index.ts

@ -31,221 +31,159 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: "/account", path: "/account",
name: "account", name: "account",
component: () => component: () => import("../views/AccountViewView.vue"),
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
}, },
{ {
path: "/claim/:id?", path: "/claim/:id?",
name: "claim", name: "claim",
component: () => component: () => import("../views/ClaimView.vue"),
import(/* webpackChunkName: "claim" */ "../views/ClaimView.vue"),
}, },
{ {
path: "/confirm-contact", path: "/confirm-contact",
name: "confirm-contact", name: "confirm-contact",
component: () => component: () => import("../views/ConfirmContactView.vue"),
import(
/* webpackChunkName: "confirm-contact" */ "../views/ConfirmContactView.vue"
),
}, },
{ {
path: "/contact-amounts", path: "/contact-amounts",
name: "contact-amounts", name: "contact-amounts",
component: () => component: () => import("../views/ContactAmountsView.vue"),
import(
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
),
}, },
{ {
path: "/contact-gives", path: "/contact-gives",
name: "contact-gives", name: "contact-gives",
component: () => component: () => import("../views/ContactGiftingView.vue"),
import(
/* webpackChunkName: "contact-gives" */ "../views/ContactGiftingView.vue"
),
}, },
{ {
path: "/contact-qr", path: "/contact-qr",
name: "contact-qr", name: "contact-qr",
component: () => component: () => import("../views/ContactQRScanShowView.vue"),
import(
/* webpackChunkName: "contact-qr" */ "../views/ContactQRScanShowView.vue"
),
}, },
{ {
path: "/contacts", path: "/contacts",
name: "contacts", name: "contacts",
component: () => component: () => import("../views/ContactsView.vue"),
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
}, },
{ {
path: "/discover", path: "/discover",
name: "discover", name: "discover",
component: () => component: () => import("../views/DiscoverView.vue"),
import(/* webpackChunkName: "discover" */ "../views/DiscoverView.vue"),
}, },
{ {
path: "/gifted-details", path: "/gifted-details",
name: "gifted-details", name: "gifted-details",
component: () => component: () => import("../views/GiftedDetails.vue"),
import(
/* webpackChunkName: "gifted-details" */ "../views/GiftedDetails.vue"
),
}, },
{ {
path: "/help", path: "/help",
name: "help", name: "help",
component: () => component: () => import("../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",
component: () => component: () => import("../views/HelpNotificationsView.vue"),
import( },
/* webpackChunkName: "help-notifications" */ "../views/HelpNotificationsView.vue" {
), path: "/help-onboarding",
name: "help-onboarding",
component: () => import("../views/HelpOnboardingView.vue"),
},
{
path: "/",
name: "home",
component: () => import("../views/HomeView.vue"),
}, },
{ {
path: "/identity-switcher", path: "/identity-switcher",
name: "identity-switcher", name: "identity-switcher",
component: () => component: () => import("../views/IdentitySwitcherView.vue"),
import(
/* webpackChunkName: "identity-switcher" */ "../views/IdentitySwitcherView.vue"
),
}, },
{ {
path: "/import-account", path: "/import-account",
name: "import-account", name: "import-account",
component: () => component: () => import("../views/ImportAccountView.vue"),
import(
/* webpackChunkName: "import-account" */ "../views/ImportAccountView.vue"
),
}, },
{ {
path: "/import-derive", path: "/import-derive",
name: "import-derive", name: "import-derive",
component: () => component: () => import("../views/ImportDerivedAccountView.vue"),
import(
/* webpackChunkName: "import-derive" */ "../views/ImportDerivedAccountView.vue"
),
}, },
{ {
path: "/new-edit-account", path: "/new-edit-account",
name: "new-edit-account", name: "new-edit-account",
component: () => component: () => import("../views/NewEditAccountView.vue"),
import(
/* webpackChunkName: "new-edit-account" */ "../views/NewEditAccountView.vue"
),
}, },
{ {
path: "/new-edit-project", path: "/new-edit-project",
name: "new-edit-project", name: "new-edit-project",
component: () => component: () => import("../views/NewEditProjectView.vue"),
import(
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
),
}, },
{ {
path: "/new-identifier", path: "/new-identifier",
name: "new-identifier", name: "new-identifier",
component: () => component: () => import("../views/NewIdentifierView.vue"),
import(
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
),
}, },
{ {
path: "/project/:id?", path: "/project/:id?",
name: "project", name: "project",
component: () => component: () => import("../views/ProjectViewView.vue"),
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
}, },
{ {
path: "/projects", path: "/projects",
name: "projects", name: "projects",
component: () => component: () => import("../views/ProjectsView.vue"),
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart, beforeEnter: enterOrStart,
}, },
{ {
path: "/quick-action-bvc", path: "/quick-action-bvc",
name: "quick-action-bvc", name: "quick-action-bvc",
component: () => component: () => import("../views/QuickActionBvcView.vue"),
import(
/* webpackChunkName: "quick-action-bvc" */ "../views/QuickActionBvcView.vue"
),
}, },
{ {
path: "/quick-action-bvc-begin", path: "/quick-action-bvc-begin",
name: "quick-action-bvc-begin", name: "quick-action-bvc-begin",
component: () => component: () => import("../views/QuickActionBvcBeginView.vue"),
import(
/* webpackChunkName: "quick-action-bvc-begin" */ "../views/QuickActionBvcBeginView.vue"
),
}, },
{ {
path: "/quick-action-bvc-end", path: "/quick-action-bvc-end",
name: "quick-action-bvc-end", name: "quick-action-bvc-end",
component: () => component: () => import("../views/QuickActionBvcEndView.vue"),
import(
/* webpackChunkName: "quick-action-bvc-end" */ "../views/QuickActionBvcEndView.vue"
),
}, },
{ {
path: "/scan-contact", path: "/scan-contact",
name: "scan-contact", name: "scan-contact",
component: () => component: () => import("../views/ContactScanView.vue"),
import(
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
),
}, },
{ {
path: "/search-area", path: "/search-area",
name: "search-area", name: "search-area",
component: () => component: () => import("../views/SearchAreaView.vue"),
import(
/* webpackChunkName: "search-area" */ "../views/SearchAreaView.vue"
),
}, },
{ {
path: "/seed-backup", path: "/seed-backup",
name: "seed-backup", name: "seed-backup",
component: () => component: () => import("../views/SeedBackupView.vue"),
import(
/* webpackChunkName: "seed-backup" */ "../views/SeedBackupView.vue"
),
}, },
{ {
path: "/start", path: "/start",
name: "start", name: "start",
component: () => component: () => import("../views/StartView.vue"),
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
}, },
{ {
path: "/statistics", path: "/statistics",
name: "statistics", name: "statistics",
component: () => component: () => import("../views/StatisticsView.vue"),
import(
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
),
}, },
{ {
path: "/test", path: "/test",
name: "test", name: "test",
component: () => component: () => import("../views/TestView.vue"),
import(/* webpackChunkName: "test" */ "../views/TestView.vue"),
}, },
]; ];
/** @type {*} */ /** @type {*} */
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes, routes,
}); });

1
src/util.d.ts

@ -1,4 +1,5 @@
// from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/util.d.ts // from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/util.d.ts
/* eslint-disable */
/** /**
* The `node:util` module supports the needs of Node.js internal APIs. Many of the * The `node:util` module supports the needs of Node.js internal APIs. Many of the
* utilities are useful for application and module developers as well. To access * utilities are useful for application and module developers as well. To access

48
src/views/AccountViewView.vue

@ -112,6 +112,8 @@
</div> </div>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<!-- label -->
<div class="mb-2 font-bold">Settings</div>
<div <div
v-if="!notificationMaybeChanged" v-if="!notificationMaybeChanged"
class="flex items-center justify-between cursor-pointer" class="flex items-center justify-between cursor-pointer"
@ -140,16 +142,38 @@
Notification status may have changed. Refresh this page to see the Notification status may have changed. Refresh this page to see the
latest setting. latest setting.
</div> </div>
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications"> <router-link class="pl-4 text-sm text-blue-500" to="/help-notifications">
Troubleshoot your notification setup. Troubleshoot your notification setup.
</router-link> </router-link>
<router-link
:to="{ name: 'search-area' }"
v-if="activeDid"
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-6"
>
Set Search Area
<!-- If already set, change button label to "Change Search Area" -->
</router-link>
<div class="text-slate-500 text-sm font-bold mt-6 mb-2">
Topics of Interest
</div>
<textarea
class="block w-full rounded border border-slate-400 px-3 py-2"
rows="3"
value="longing, rusted, seventeen, daybreak, furnace, nine, benign, homecoming, one, freight car"
>
</textarea>
<div class="text-slate-500 text-sm mt-2 mb-2">
Separate topics with a comma.
</div>
</div> </div>
<div <div
v-if="activeDid" v-if="activeDid"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8" class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
> >
<div class="mb-2">Usage Limits</div> <div class="mb-2 font-bold">Usage Limits</div>
<!-- show spinner if loading limits --> <!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center"> <div v-if="loadingLimits" class="text-center">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa> Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
@ -200,7 +224,7 @@
</div> </div>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
<div>Data Export</div> <div class="mb-2 font-bold">Data Export</div>
<router-link <router-link
:to="{ name: 'seed-backup' }" :to="{ name: 'seed-backup' }"
v-if="activeDid" v-if="activeDid"
@ -211,7 +235,7 @@
<button <button
v-bind:class="computedStartDownloadLinkClassNames()" v-bind:class="computedStartDownloadLinkClassNames()"
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" 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="exportDatabase()" @click="exportDatabase()"
> >
Download Settings & Contacts Download Settings & Contacts
@ -221,7 +245,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-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" 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 mb-6"
> >
If no download happened yet, click again here to download now. If no download happened yet, click again here to download now.
</a> </a>
@ -503,7 +527,7 @@
<button> <button>
<router-link <router-link
:to="{ name: 'statistics' }" :to="{ name: 'statistics' }"
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" 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"
> >
See Global Animated History of Giving See Global Animated History of Giving
</router-link> </router-link>
@ -539,9 +563,7 @@ import {
EndorserRateLimits, EndorserRateLimits,
ImageRateLimits, ImageRateLimits,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Buffer } from "buffer/";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface IAccount { interface IAccount {
did: string; did: string;
@ -952,13 +974,13 @@ export default class AccountViewView extends Vue {
public computedStartDownloadLinkClassNames() { public computedStartDownloadLinkClassNames() {
return { return {
invisible: this.downloadUrl, hidden: this.downloadUrl,
}; };
} }
public computedDownloadLinkClassNames() { public computedDownloadLinkClassNames() {
return { return {
invisible: !this.downloadUrl, hidden: !this.downloadUrl,
}; };
} }
@ -1146,9 +1168,9 @@ export default class AccountViewView extends Vue {
this.limitsMessage = this.limitsMessage =
(data?.error?.message as string) || "Bad server response."; (data?.error?.message as string) || "Bad server response.";
console.error( console.error(
"Got bad response retrieving limits, which usually means user isn't registered:", "Got bad response retrieving limits, which usually means user isn't registered.",
error,
); );
//console.error(error);
} else { } else {
this.limitsMessage = "Got an error retrieving limits."; this.limitsMessage = "Got an error retrieving limits.";
console.error("Got some error retrieving limits:", error); console.error("Got some error retrieving limits:", error);

24
src/views/ConfirmContactView.vue

@ -30,17 +30,19 @@
</div> </div>
<div class="mt-8"> <div class="mt-8">
<input <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
type="submit" <input
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" type="submit"
value="Add Contact" 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"
/> value="Add Contact"
<button />
type="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" type="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"
Cancel >
</button> Cancel
</button>
</div>
</div> </div>
</section> </section>
</template> </template>

10
src/views/ContactAmountsView.vue

@ -119,7 +119,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { import {
AgreeVerifiableCredential, AgreeVerifiableCredential,
GiveServerRecord, GiveSummaryRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT, SCHEMA_ORG_CONTEXT,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
@ -131,7 +131,7 @@ export default class ContactAmountssView extends Vue {
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contact: Contact | null = null; contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = []; giveRecords: Array<GiveSummaryRecord> = [];
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
@ -197,7 +197,7 @@ export default class ContactAmountssView extends Vue {
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
let result: Array<GiveServerRecord> = []; let result: Array<GiveSummaryRecord> = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
@ -252,7 +252,7 @@ export default class ContactAmountssView extends Vue {
); );
} }
const sortedResult: Array<GiveServerRecord> = R.sort( const sortedResult: Array<GiveSummaryRecord> = R.sort(
(a, b) => (a, b) =>
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(), new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
result, result,
@ -271,7 +271,7 @@ export default class ContactAmountssView extends Vue {
} }
} }
async confirm(record: GiveServerRecord) { async confirm(record: GiveSummaryRecord) {
// Make claim // Make claim
// I use clone here because otherwise it gets a Proxy object. // I use clone here because otherwise it gets a Proxy object.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

4
src/views/ContactQRScanShowView.vue

@ -88,9 +88,7 @@ import {
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
ENDORSER_JWT_URL_LOCATION, ENDORSER_JWT_URL_LOCATION,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Buffer } from "buffer/";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@Component({ @Component({
components: { components: {

24
src/views/ContactScanView.vue

@ -65,17 +65,19 @@
/> />
<div class="mt-8"> <div class="mt-8">
<input <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
type="submit" <input
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" type="submit"
value="Look Up Contact" 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"
/> value="Look Up Contact"
<button />
type="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" type="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"
Cancel >
</button> Cancel
</button>
</div>
</div> </div>
</section> </section>
</template> </template>

58
src/views/ContactsView.vue

@ -10,7 +10,8 @@
<span /> <span />
<span> <span>
<a <a
@click="showHintsForOnboarding()" href="/help-onboarding"
target="_blank"
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" 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
@ -302,7 +303,7 @@ import {
import { import {
CONTACT_CSV_HEADER, CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
GiveServerRecord, GiveSummaryRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
isDid, isDid,
RegisterVerifiableCredential, RegisterVerifiableCredential,
@ -313,8 +314,7 @@ 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";
// eslint-disable-next-line @typescript-eslint/no-var-requires import { Buffer } from "buffer/";
const Buffer = require("buffer/").Buffer;
@Component({ @Component({
components: { QuickNav, EntityIcon }, components: { QuickNav, EntityIcon },
@ -409,7 +409,7 @@ export default class ContactsView extends Vue {
} }
const handleResponse = ( const handleResponse = (
resp: { status: number; data: { data: GiveServerRecord[] } }, resp: { status: number; data: { data: GiveSummaryRecord[] } },
descriptions: Record<string, string>, descriptions: Record<string, string>,
confirmed: Record<string, number>, confirmed: Record<string, number>,
unconfirmed: Record<string, number>, unconfirmed: Record<string, number>,
@ -510,18 +510,6 @@ export default class ContactsView extends Vue {
} }
} }
showHintsForOnboarding() {
this.$notify(
{
group: "alert",
type: "info",
title: "Onboard Someone",
text: libsUtil.ONBOARD_MESSAGE,
},
-1,
);
}
async onClickNewContact(): Promise<void> { async onClickNewContact(): Promise<void> {
if (!this.contactInput) { if (!this.contactInput) {
this.$notify( this.$notify(
@ -531,7 +519,7 @@ export default class ContactsView extends Vue {
title: "No Contact", title: "No Contact",
text: "There was no contact info to add.", text: "There was no contact info to add.",
}, },
-1, 3000,
); );
return; return;
} }
@ -559,7 +547,7 @@ export default class ContactsView extends Vue {
title: "Contacts Added", title: "Contacts Added",
text: "Each contact was added. Nothing was sent to the server.", text: "Each contact was added. Nothing was sent to the server.",
}, },
-1, // keeping it up so that the "visibility" message is seen 3000, // keeping it up so that the "visibility" message is seen
); );
} catch (e) { } catch (e) {
this.$notify( this.$notify(
@ -664,7 +652,7 @@ export default class ContactsView extends Vue {
title: "No Contact Info", title: "No Contact Info",
text: "The contact info could not be parsed.", text: "The contact info could not be parsed.",
}, },
-1, 3000,
); );
return; return;
} else { } else {
@ -686,7 +674,7 @@ export default class ContactsView extends Vue {
title: "Incomplete Contact", title: "Incomplete Contact",
text: "Cannot add a contact without a DID.", text: "Cannot add a contact without a DID.",
}, },
-1, 5000,
); );
return; return;
} }
@ -698,7 +686,7 @@ export default class ContactsView extends Vue {
title: "Invalid DID", title: "Invalid DID",
text: "The DID is not valid. It must begin with 'did:'", text: "The DID is not valid. It must begin with 'did:'",
}, },
-1, 5000,
); );
return; return;
} }
@ -737,7 +725,7 @@ export default class ContactsView extends Vue {
title: "Contact Added", title: "Contact Added",
text: addedMessage, text: addedMessage,
}, },
-1, // keeping it up so that the "visibility" message is seen 3000,
); );
}) })
.catch((err) => { .catch((err) => {
@ -853,7 +841,7 @@ export default class ContactsView extends Vue {
title: "Registration Still Unknown", title: "Registration Still Unknown",
text: message, text: message,
}, },
-1, 5000,
); );
} else if (resp.data?.success?.handleId) { } else if (resp.data?.success?.handleId) {
contact.registered = true; contact.registered = true;
@ -892,7 +880,7 @@ export default class ContactsView extends Vue {
title: "Registration Error", title: "Registration Error",
text: userMessage, text: userMessage,
}, },
-1, 5000,
); );
} }
} }
@ -933,7 +921,7 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
}, },
-1, 3000,
); );
} }
contact.seesMe = visibility; contact.seesMe = visibility;
@ -953,7 +941,7 @@ export default class ContactsView extends Vue {
title: "Error Setting Visibility", title: "Error Setting Visibility",
text: message, text: message,
}, },
-1, 5000,
); );
} }
} catch (err) { } catch (err) {
@ -965,7 +953,7 @@ export default class ContactsView extends Vue {
title: "Error Setting Visibility", title: "Error Setting Visibility",
text: "Check connectivity and try again.", text: "Check connectivity and try again.",
}, },
-1, 5000,
); );
} }
} }
@ -997,7 +985,7 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
}, },
-1, 3000,
); );
} else { } else {
console.error("Got bad server response checking visibility:", resp); console.error("Got bad server response checking visibility:", resp);
@ -1009,7 +997,7 @@ export default class ContactsView extends Vue {
title: "Error Checking Visibility", title: "Error Checking Visibility",
text: message, text: message,
}, },
-1, 5000,
); );
} }
} catch (err) { } catch (err) {
@ -1021,7 +1009,7 @@ export default class ContactsView extends Vue {
title: "Error Checking Visibility", title: "Error Checking Visibility",
text: "Check connectivity and try again.", text: "Check connectivity and try again.",
}, },
-1, 3000,
); );
} }
} }
@ -1069,7 +1057,7 @@ export default class ContactsView extends Vue {
title: "Input Error", title: "Input Error",
text: "This is not a valid number of hours: " + this.hourInput, text: "This is not a valid number of hours: " + this.hourInput,
}, },
-1, 3000,
); );
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) { } else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
this.$notify( this.$notify(
@ -1079,7 +1067,7 @@ export default class ContactsView extends Vue {
title: "Input Error", title: "Input Error",
text: "Giving no hours or description does nothing.", text: "Giving no hours or description does nothing.",
}, },
-1, 3000,
); );
} else if (!identity) { } else if (!identity) {
this.$notify( this.$notify(
@ -1089,7 +1077,7 @@ export default class ContactsView extends Vue {
title: "Status Error", title: "Status Error",
text: "No identifier is available.", text: "No identifier is available.",
}, },
-1, 3000,
); );
} else { } else {
// ask to confirm amount // ask to confirm amount
@ -1218,7 +1206,7 @@ export default class ContactsView extends Vue {
title: "Error Sending Give", title: "Error Sending Give",
text: userMessage, text: userMessage,
}, },
-1, 5000,
); );
} }
} }

105
src/views/GiftedDetails.vue

@ -92,19 +92,26 @@
<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
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p> </p>
<button <div class="grid grid-cols-1 sm:grid-cols-2 gap-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" <button
@click="confirm" 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"
> @click="confirm"
Sign &amp; Send >
</button> 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" <button
@click="cancel" 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> Cancel
</button>
</div>
</section> </section>
</template> </template>
@ -288,27 +295,6 @@ export default class GiftedDetails extends Vue {
} }
async confirm() { 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) { if (!this.activeDid) {
this.$notify( this.$notify(
{ {
@ -317,12 +303,23 @@ export default class GiftedDetails extends Vue {
title: "Error", title: "Error",
text: "You must select an identifier before you can record a give.", text: "You must select an identifier before you can record a give.",
}, },
-1, 2000,
); );
return; return;
} }
if (parseFloat(this.amountInput) < 0) {
if (!this.description && !this.amountInput) { this.$notify(
{
group: "alert",
type: "danger",
text: "You may not send a negative number.",
title: "",
},
2000,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -332,11 +329,33 @@ export default class GiftedDetails extends Vue {
this.libsUtil.UNIT_LONG[this.unitCode] this.libsUtil.UNIT_LONG[this.unitCode]
}.`, }.`,
}, },
-1, 2000,
); );
return; return;
} }
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() {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
@ -424,5 +443,17 @@ export default class GiftedDetails extends Vue {
result.response?.data?.error?.message result.response?.data?.error?.message
); );
} }
explainData() {
this.$notify(
{
group: "alert",
type: "success",
title: "Data Sharing",
text: libsUtil.PRIVACY_MESSAGE,
},
-1,
);
}
} }
</script> </script>

13
src/views/HelpNotificationsView.vue

@ -301,12 +301,13 @@ import { sendTestThroughPushServer } from "@/libs/util";
export default class HelpNotificationsView extends Vue { export default class HelpNotificationsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
subscription: PushSubscription | null = null; subscriptionJSON?: PushSubscriptionJSON;
async mounted() { async mounted() {
try { try {
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription(); const fullSub = await registration.pushManager.getSubscription();
this.subscriptionJSON = fullSub?.toJSON();
} catch (error) { } catch (error) {
console.error("Mount error:", error); console.error("Mount error:", error);
} }
@ -315,13 +316,13 @@ export default class HelpNotificationsView extends Vue {
alertWebPushSubscription() { alertWebPushSubscription() {
console.log( console.log(
"Web push subscription:", "Web push subscription:",
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging JSON.parse(JSON.stringify(this.subscriptionJSON)), // gives more info than plain console logging
); );
alert(JSON.stringify(this.subscription)); alert(JSON.stringify(this.subscriptionJSON));
} }
async sendTestWebPushMessage(skipFilter: boolean = false) { async sendTestWebPushMessage(skipFilter: boolean = false) {
if (!this.subscription) { if (!this.subscriptionJSON) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -336,7 +337,7 @@ export default class HelpNotificationsView extends Vue {
} }
try { try {
await sendTestThroughPushServer(this.subscription, skipFilter); await sendTestThroughPushServer(this.subscriptionJSON, skipFilter);
this.$notify( this.$notify(
{ {

69
src/views/HelpOnboardingView.vue

@ -0,0 +1,69 @@
<template>
<!-- Don't include nav buttons since this is shown in a different window. -->
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Don't include 'back' button since this is shown in a different window. -->
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Time Safari Onboarding Instructions
</h1>
</div>
<!-- eslint-disable prettier/prettier -->
<div class="ml-4">
<h1 class="font-bold text-xl">Install</h1>
<div>
<p>
1) Have them visit TimeSafari.app in a browser, preferably Chrome or Safari.
</p>
<p>
2) Have them "Install" the site to their desktop.
</p>
</div>
<h1 class="font-bold text-xl">Add Contact & Register</h1>
<div>
<p>
3) Have them follow their yellow prompts.
</p>
<p>
4) Add them to your contacts <fa icon="users" />
</p>
<p>
5) Register them <fa icon="person-circle-question" />
</p>
<p>
6) Add yourself to their contacts <fa icon="users" />
</p>
</div>
<h1 class="font-bold text-xl">Enable Notifications</h1>
<div>
<p>
7) Enable notifications from <fa icon="circle-user" />
</p>
</div>
<h1 class="font-bold text-xl">Discuss Backups</h1>
<div>
<p>
8) Exporting backups <fa icon="circle-user" /> are important if they lose their phone --- especially for the Identifier Seed!
</p>
</div>
</div>
<!-- eslint enable -->
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class Help extends Vue {}
</script>

39
src/views/HelpView.vue

@ -39,22 +39,22 @@
and network. and network.
</p> </p>
<p> <p>
You can show giving and also offer help to ideas, based on others' You highlight giving and also offer help to ideas -- which could be
willingness to help out, too. You can record your own ideas and invite conditional on others' willingness to help, too.
others to collaborate. You can record your own ideas and invite others to collaborate.
</p> </p>
<p> <p>
This app uses the power of cryptography to build a reputation, recording This app uses the power of cryptography to build a reputation, recording
activity that you can share at your discretion. You put some activity activity that you can share at your discretion. You put some activity
public, but your sensitive information is not shared with anyone, public, but these services don't share your ID with others without explicit consent.
including our services. This is in contrast to Meta and Google, who hold This is in contrast to Meta and Google, who hold
your data and allow you use it. Those services are useful, but they have your data and allow you use it while they manage sharing...
the control; this app gives you the control. those services are useful but they have the control, whereas this app gives you the control.
</p> </p>
<h2 class="text-xl font-semibold">How do I get started?</h2> <h2 class="text-xl font-semibold">How do I get started?</h2>
<p> <p>
You need someone to register you -- usually the person who told you You need someone to register you, like the person who told you
about this app, on the Contacts about this app, on the Contacts
<fa icon="users" class="fa-fw" /> page. After they register you, you can <fa icon="users" class="fa-fw" /> page. After they register you, you can
select any contact on the home page (or "anonymous") and record your select any contact on the home page (or "anonymous") and record your
@ -83,9 +83,9 @@
<h2 class="text-xl font-semibold">How do I add someone else?</h2> <h2 class="text-xl font-semibold">How do I add someone else?</h2>
<p> <p>
<button class="text-blue-500" @click="showOnboardInfo"> <a href="/help-onboarding" target="_blank" class="text-blue-500">
Click here to show an alert with the steps. Click here to show an alert with the steps.
</button> </a>
To start scanning, go To start scanning, go
<router-link class="text-blue-500" to="/contact-qr">here.</router-link> <router-link class="text-blue-500" to="/contact-qr">here.</router-link>
</p> </p>
@ -198,9 +198,7 @@
<ul> <ul>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
Chrome: Chrome:
<a href="chrome://settings/content/all" class="text-blue-500" Clear at chrome://settings/content/all and
>clear here</a
>
also clear under dev tools Application also clear under dev tools Application
</li> </li>
<li class="list-disc list-outside ml-4"> <li class="list-disc list-outside ml-4">
@ -378,25 +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 { NotificationIface } from "@/constants/app";
import { ONBOARD_MESSAGE } from "@/libs/util";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
package = Package; package = Package;
commitHash = process.env.VUE_APP_GIT_HASH; commitHash = import.meta.env.VITE_GIT_HASH;
showOnboardInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "Onboard Someone",
text: ONBOARD_MESSAGE,
},
-1,
);
}
} }
</script> </script>

200
src/views/HomeView.vue

@ -118,7 +118,9 @@
<h2 class="text-xl font-bold">Record Something Given By:</h2> <h2 class="text-xl font-bold">Record Something Given By:</h2>
</div> </div>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
>
<li @click="openDialog()"> <li @click="openDialog()">
<img <img
src="../assets/blank-square.svg" src="../assets/blank-square.svg"
@ -158,7 +160,7 @@
</router-link> </router-link>
<button <button
@click="openGiftedPrompts()" @click="openGiftedPrompts()"
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="block 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"
> >
Ideas... Ideas...
</button> </button>
@ -172,10 +174,29 @@
showGivenToUser="true" showGivenToUser="true"
/> />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" />
<!-- Results List --> <!-- Results List -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h2 class="text-xl font-bold mb-4">Latest Activity</h2> <div class="flex items-center mb-4">
<h2 class="text-xl font-bold">Latest Activity</h2>
<button @click="openFeedFilters()" class="block text-center ml-auto">
<span class="text-sm uppercase text-white">
<span
v-if="resultsAreFiltered()"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
>
Filtered
</span>
<span
v-else
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md"
>
Unfiltered
</span>
</span>
</button>
</div>
<InfiniteScroll @reached-bottom="loadMoreGives"> <InfiniteScroll @reached-bottom="loadMoreGives">
<ul class="border-t border-slate-300"> <ul class="border-t border-slate-300">
<li <li
@ -191,37 +212,36 @@
</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-1 justify-self-start">
<span> <span>
<fa <fa
v-if="record.giver.known || record.receiver.known" v-if="record.giver.known || record.receiver.known"
icon="circle-user" icon="circle-user"
class="col-span-1 pt-1 pl-0 pr-3 text-slate-500" class="pt-1 text-slate-500"
/>
<fa
v-else
icon="gift"
class="col-span-1 pt-1 pl-3 pr-0 text-slate-500"
/> />
<fa v-else icon="gift" class="pt-1 pl-3 text-slate-500" />
</span>
</span>
<span class="col-span-10 justify-self-stretch">
<span class="pl-2">
{{ giveDescription(record) }}
</span> </span>
{{ giveDescription(record) }}
<a @click="onClickLoadClaim(record.jwtId)"> <a @click="onClickLoadClaim(record.jwtId)">
<fa <fa
icon="circle-info" icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
></fa> ></fa>
</a> </a>
</span> </span>
<span class="col-span-1 justify-self-end shrink"> <span class="col-span-1 justify-self-end">
<router-link <router-link
v-if="record.fulfillsPlanHandleId" v-if="record.fulfillsPlanHandleId"
:to=" :to="
'/project/' + '/project/' +
encodeURIComponent(record.fulfillsPlanHandleId) encodeURIComponent(record.fulfillsPlanHandleId)
" "
class="justify-end"
> >
<fa icon="hammer" class="ml-4 pl-2 text-blue-500"></fa> <fa icon="hammer" class="text-blue-500"></fa>
</router-link> </router-link>
</span> </span>
</div> </div>
@ -238,11 +258,17 @@
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip; <fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</p> </p>
</div> </div>
<div v-if="!isFeedLoading && feedData.length === 0">
<p class="text-slate-500 text-center italic mt-4 mb-4">
No claims match your filters.
</p>
</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda";
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
@ -250,6 +276,7 @@ import { Component, Vue } from "vue-facing-decorator";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPrompts from "@/components/GiftedPrompts.vue"; import GiftedPrompts from "@/components/GiftedPrompts.vue";
import FeedFilters from "@/components/FeedFilters.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";
@ -257,22 +284,30 @@ 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 {
BoundingBox,
isAnyFeedFilterOn,
MASTER_SETTINGS_KEY,
Settings,
} from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
contactForDid, contactForDid,
containsNonHiddenDid,
didInfoForContact, didInfoForContact,
getPlanFromCache,
GiverInputInfo, GiverInputInfo,
GiveServerRecord, GiveSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { generateSaveAndActivateIdentity } from "@/libs/util"; import { generateSaveAndActivateIdentity } from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveServerRecord { interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: { giver: {
displayName: string; displayName: string;
known: boolean; known: boolean;
}; };
image: string; image: string;
recipientProjectName: string | undefined;
receiver: { receiver: {
displayName: string; displayName: string;
known: boolean; known: boolean;
@ -283,6 +318,7 @@ interface GiveRecordWithContactInfo extends GiveServerRecord {
components: { components: {
GiftedDialog, GiftedDialog,
GiftedPrompts, GiftedPrompts,
FeedFilters,
QuickNav, QuickNav,
EntityIcon, EntityIcon,
InfiniteScroll, InfiniteScroll,
@ -299,9 +335,16 @@ export default class HomeView extends Vue {
feedData: GiveRecordWithContactInfo[] = []; feedData: GiveRecordWithContactInfo[] = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedClaimId?: string;
isAnyFeedFilterOn: boolean;
isCreatingIdentifier = false; isCreatingIdentifier = false;
isFeedFilteredByVisible = false;
isFeedFilteredByNearby = false;
isFeedLoading = true; isFeedLoading = true;
isRegistered = false; isRegistered = false;
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = 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
@ -324,7 +367,7 @@ export default class HomeView extends Vue {
return headers; return headers;
} }
async created() { async mounted() {
try { try {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
@ -336,9 +379,14 @@ export default class HomeView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.searchBoxes = settings?.searchBoxes || [];
this.showShortcutBvc = !!settings?.showShortcutBvc; this.showShortcutBvc = !!settings?.showShortcutBvc;
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
if (this.allMyDids.length === 0) { if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true; this.isCreatingIdentifier = true;
this.activeDid = await generateSaveAndActivateIdentity(); this.activeDid = await generateSaveAndActivateIdentity();
@ -367,6 +415,10 @@ export default class HomeView extends Vue {
} }
} }
resultsAreFiltered() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
}
notificationsSupported() { notificationsSupported() {
return "Notification" in window; return "Notification" in window;
} }
@ -376,27 +428,34 @@ export default class HomeView extends Vue {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
const identity = await this.getIdentity(this.activeDid);
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); if (identity) {
const allAccounts = await accountsDB.accounts.toArray(); headers["Authorization"] = "Bearer " + (await accessToken(identity));
const account = allAccounts.find( } else {
(acc) => acc.did === this.activeDid,
) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error( throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.", "An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
); );
} }
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else { } else {
// it's OK without auth... we just won't get any identifiers // it's OK without auth... we just won't get any identifiers
} }
return headers; return headers;
} }
// only called when a setting was changed
async reloadFeedOnChange() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
this.feedData = [];
this.feedPreviousOldestId = undefined;
this.updateAllFeed();
}
/** /**
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
@ -407,14 +466,30 @@ export default class HomeView extends Vue {
} }
} }
latLongInAnySearchBox(lat: number, long: number) {
for (const boxInfo of this.searchBoxes) {
if (
boxInfo.bbox.westLong <= long &&
long <= boxInfo.bbox.eastLong &&
boxInfo.bbox.minLat <= lat &&
lat <= boxInfo.bbox.maxLat
) {
return true;
}
}
}
public async updateAllFeed() { public async updateAllFeed() {
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true;
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) {
endOfResults = false;
// include the descriptions of the giver and receiver // include the descriptions of the giver and receiver
const newFeedData: GiveRecordWithContactInfo = results.data.map( const identity = await this.getIdentity(this.activeDid);
(record: GiveServerRecord) => { const newFeedData: Array<Promise<GiveRecordWithContactInfo>> =
results.data.map(async (record: GiveSummaryRecord) => {
// 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
@ -425,6 +500,36 @@ export default class HomeView extends Vue {
// recipient.did is for legacy data, before March 2023 // recipient.did is for legacy data, before March 2023
const recipientDid = const recipientDid =
claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any claim.recipient?.identifier || (claim.recipient as any)?.did; // eslint-disable-line @typescript-eslint/no-explicit-any
const plan = await getPlanFromCache(
record.fulfillsPlanHandleId,
identity,
this.axios,
this.apiServer,
);
// check if the record should be filtered out
let anyMatch = false;
if (
this.isFeedFilteredByVisible &&
containsNonHiddenDid(record)
) {
// has a visible DID so it's a keeper
anyMatch = true;
}
if (!anyMatch && this.isFeedFilteredByNearby) {
// check if the associated project has a location inside user's search box
if (record.fulfillsPlanHandleId) {
if (plan?.locLat && plan?.locLon) {
if (this.latLongInAnySearchBox(plan.locLat, plan.locLon)) {
anyMatch = true;
}
}
}
}
if (this.isAnyFeedFilterOn && !anyMatch) {
return null;
}
return { return {
...record, ...record,
giver: didInfoForContact( giver: didInfoForContact(
@ -434,6 +539,7 @@ export default class HomeView extends Vue {
this.allMyDids, this.allMyDids,
), ),
image: claim.image, image: claim.image,
recipientProjectName: plan?.name,
receiver: didInfoForContact( receiver: didInfoForContact(
recipientDid, recipientDid,
this.activeDid, this.activeDid,
@ -441,9 +547,11 @@ export default class HomeView extends Vue {
this.allMyDids, this.allMyDids,
), ),
}; };
}, });
); const allNewFeedData: GiveRecordWithContactInfo[] =
this.feedData = this.feedData.concat(newFeedData); await Promise.all(newFeedData);
const filteredFeedData = allNewFeedData.filter(R.isNotNil);
this.feedData = this.feedData.concat(filteredFeedData);
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.
@ -470,6 +578,10 @@ export default class HomeView extends Vue {
-1, -1,
); );
}); });
if (this.feedData.length === 0 && !endOfResults) {
// repeat until there's at least some data
this.updateAllFeed();
}
this.isFeedLoading = false; this.isFeedLoading = false;
} }
@ -536,12 +648,28 @@ export default class HomeView extends Vue {
return `${giverInfo.displayName} gave to ${recipientInfo.displayName}: ${gaveAmount}`; return `${giverInfo.displayName} gave to ${recipientInfo.displayName}: ${gaveAmount}`;
} else if (giverInfo.known) { } else if (giverInfo.known) {
// giver is named but recipient is not // giver is named but recipient is not
// show the project name if to one
if (giveRecord.recipientProjectName) {
// retrieve the project name
return `${giverInfo.displayName} gave: ${gaveAmount} (to the project ${giveRecord.recipientProjectName})`;
}
// it's not to a project
return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`; return `${giverInfo.displayName} gave: ${gaveAmount} (to ${recipientInfo.displayName})`;
} else if (recipientInfo.known) { } else if (recipientInfo.known) {
// recipient is named but giver is not // recipient is named but giver is not
return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`; return `${recipientInfo.displayName} received: ${gaveAmount} (from ${giverInfo.displayName})`;
} else { } else {
// neither giver nor recipient are named // neither giver nor recipient are named
// show the project name if to one
if (giveRecord.recipientProjectName) {
// retrieve the project name
return `${gaveAmount} (to the project ${giveRecord.recipientProjectName})`;
}
// it's not to a project
let peopleInfo; let peopleInfo;
if (giverInfo.displayName === recipientInfo.displayName) { if (giverInfo.displayName === recipientInfo.displayName) {
peopleInfo = `between two who are ${giverInfo.displayName}`; peopleInfo = `between two who are ${giverInfo.displayName}`;
@ -574,5 +702,9 @@ export default class HomeView extends Vue {
openGiftedPrompts() { openGiftedPrompts() {
(this.$refs.giftedPrompts as GiftedPrompts).open(); (this.$refs.giftedPrompts as GiftedPrompts).open();
} }
openFeedFilters() {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
}
} }
</script> </script>

28
src/views/ImportAccountView.vue

@ -56,19 +56,21 @@
</div> </div>
<div class="mt-8"> <div class="mt-8">
<button <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
@click="fromMnemonic()" <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="fromMnemonic()"
> 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"
Import >
</button> Import
<button </button>
@click="onCancelClick()" <button
type="button" @click="onCancelClick()"
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" type="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"
Cancel >
</button> Cancel
</button>
</div>
</div> </div>
</section> </section>
</template> </template>

28
src/views/ImportDerivedAccountView.vue

@ -49,19 +49,21 @@
</ul> </ul>
</div> </div>
<div class="mt-8"> <div class="mt-8">
<button <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
@click="incrementDerivation()" <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="incrementDerivation()"
> 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"
Increment and Import >
</button> Increment and Import
<button </button>
@click="onCancelClick()" <button
type="button" @click="onCancelClick()"
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" type="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"
Cancel >
</button> Cancel
</button>
</div>
</div> </div>
</section> </section>
</template> </template>

32
src/views/NewEditAccountView.vue

@ -22,21 +22,23 @@
/> />
<div class="mt-8"> <div class="mt-8">
<button <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
type="button" <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" type="button"
@click="onClickSaveChanges()" 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()"
Save Changes >
</button> Save Changes
<!-- SHOW ME instead while processing saving changes --> </button>
<button <!-- SHOW ME instead while processing saving changes -->
type="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" type="button"
@click="onClickCancel()" 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()"
Cancel >
</button> Cancel
</button>
</div>
</div> </div>
</section> </section>
</template> </template>

53
src/views/NewEditProjectView.vue

@ -73,16 +73,17 @@
/> />
<label for="includeLocation">Include Location</label> <label for="includeLocation">Include Location</label>
</div> </div>
<div v-if="includeLocation" style="height: 600px; width: 800px"> <div v-if="includeLocation" class="mb-4 aspect-video">
<div class="px-2 py-2"> <p class="text-sm mb-2 text-slate-500">
For your security, choose a location nearby but not exactly at the For your security, choose a location nearby but not exactly at the
place. place.
</div> </p>
<l-map <l-map
ref="map" ref="map"
v-model:zoom="zoom" v-model:zoom="zoom"
:center="[0, 0]" :center="[0, 0]"
class="!z-40 rounded-md"
@click=" @click="
(event) => { (event) => {
latitude = event.latlng.lat; latitude = event.latlng.lat;
@ -104,28 +105,30 @@
</div> </div>
<div class="mt-8"> <div class="mt-8">
<button <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
:disabled="isHiddenSave" <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" :disabled="isHiddenSave"
@click="onSaveProjectClick()" 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()"
<!-- SHOW if in idle state --> >
<span :class="{ hidden: isHiddenSave }">Save Project</span> <!-- SHOW if in idle state -->
<span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state --> <!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }"> <span :class="{ hidden: isHiddenSpinner }">
<!-- icon no worky? --> <!-- icon no worky? -->
<i class="fa-solid fa-spinner fa-spin-pulse"></i> <i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving...</span Saving...</span
>
</button>
<button
type="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="onCancelClick()"
> >
</button> Cancel
<button </button>
type="button" </div>
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()"
>
Cancel
</button>
</div> </div>
</section> </section>
</template> </template>
@ -265,6 +268,8 @@ export default class NewEditProjectView extends Vue {
vcClaim.agent = { vcClaim.agent = {
identifier: this.agentDid, identifier: this.agentDid,
}; };
} else {
delete vcClaim.agent;
} }
if (this.includeLocation) { if (this.includeLocation) {
vcClaim.location = { vcClaim.location = {
@ -274,6 +279,8 @@ export default class NewEditProjectView extends Vue {
longitude: this.longitude, longitude: this.longitude,
}, },
}; };
} else {
delete vcClaim.location;
} }
// Make a payload for the claim // Make a payload for the claim
const vcPayload = { const vcPayload = {

40
src/views/ProjectViewView.vue

@ -98,7 +98,7 @@
</div> </div>
<a @click="onClickLoadClaim(projectId)" class="cursor-pointer"> <a @click="onClickLoadClaim(projectId)" class="cursor-pointer">
<fa icon="circle-info" class="pl-2 pt-1 text-blue-500" /> <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a> </a>
</div> </div>
@ -134,7 +134,9 @@
<div class="text-center"> <div class="text-center">
<p class="mt-2 mb-4 text-center">Record a contribution from:</p> <p class="mt-2 mb-4 text-center">Record a contribution from:</p>
</div> </div>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
>
<li @click="openGiftDialog({ name: 'you', did: activeDid })"> <li @click="openGiftDialog({ name: 'you', did: activeDid })">
<fa icon="hand" class="fa-fw text-slate-400 text-5xl" /> <fa icon="hand" class="fa-fw text-slate-400 text-5xl" />
<h3 <h3
@ -230,7 +232,7 @@
@click="onClickLoadClaim(offer.jwtId as string)" @click="onClickLoadClaim(offer.jwtId as string)"
class="cursor-pointer" class="cursor-pointer"
> >
<fa icon="circle-info" class="pl-2 pt-1 text-blue-500" /> <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a> </a>
<a <a
v-if="checkIsFulfillable(offer)" v-if="checkIsFulfillable(offer)"
@ -289,7 +291,7 @@
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)"> <a @click="onClickLoadClaim(give.jwtId)">
<fa icon="circle-info" class="text-blue-500 cursor-pointer" /> <fa icon="file-lines" class="text-blue-500 cursor-pointer" />
</a> </a>
<a v-if="checkIsConfirmable(give)" @click="confirmClaim(give)"> <a v-if="checkIsConfirmable(give)" @click="confirmClaim(give)">
<fa icon="circle-check" class="text-blue-500 cursor-pointer" /> <fa icon="circle-check" class="text-blue-500 cursor-pointer" />
@ -360,11 +362,11 @@ import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { import {
BLANK_GENERIC_SERVER_RECORD, BLANK_GENERIC_SERVER_RECORD,
GenericServerRecord, GenericCredWrapper,
GiverInputInfo, GiverInputInfo,
GiveServerRecord, GiveSummaryRecord,
OfferServerRecord, OfferSummaryRecord,
PlanServerRecord, PlanSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
@ -388,14 +390,14 @@ export default class ProjectViewView extends Vue {
apiServer = ""; apiServer = "";
description = ""; description = "";
expanded = false; expanded = false;
fulfilledByThis: PlanServerRecord | null = null; fulfilledByThis: PlanSummaryRecord | null = null;
fulfillersToThis: Array<PlanServerRecord> = []; fulfillersToThis: Array<PlanSummaryRecord> = [];
givesToThis: Array<GiveServerRecord> = []; givesToThis: Array<GiveSummaryRecord> = [];
issuer = ""; issuer = "";
latitude = 0; latitude = 0;
longitude = 0; longitude = 0;
name = ""; name = "";
offersToThis: Array<OfferServerRecord> = []; offersToThis: Array<OfferSummaryRecord> = [];
projectId = localStorage.getItem("projectId") || ""; // handle ID projectId = localStorage.getItem("projectId") || ""; // handle ID
showDidCopy = false; showDidCopy = false;
timeSince = ""; timeSince = "";
@ -718,8 +720,8 @@ export default class ProjectViewView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
checkIsFulfillable(offer: OfferServerRecord) { checkIsFulfillable(offer: OfferSummaryRecord) {
const offerRecord: GenericServerRecord = { const offerRecord: GenericCredWrapper = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: offer.fullClaim, claim: offer.fullClaim,
claimType: "Offer", claimType: "Offer",
@ -728,8 +730,8 @@ export default class ProjectViewView extends Vue {
return libsUtil.canFulfillOffer(offerRecord); return libsUtil.canFulfillOffer(offerRecord);
} }
onClickFulfillGiveToOffer(offer: OfferServerRecord) { onClickFulfillGiveToOffer(offer: OfferSummaryRecord) {
const offerRecord: GenericServerRecord = { const offerRecord: GenericCredWrapper = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: offer.fullClaim, claim: offer.fullClaim,
issuer: offer.offeredByDid, issuer: offer.offeredByDid,
@ -768,8 +770,8 @@ export default class ProjectViewView extends Vue {
} }
} }
checkIsConfirmable(give: GiveServerRecord) { checkIsConfirmable(give: GiveSummaryRecord) {
const giveDetails: GenericServerRecord = { const giveDetails: GenericCredWrapper = {
...BLANK_GENERIC_SERVER_RECORD, ...BLANK_GENERIC_SERVER_RECORD,
claim: give.fullClaim, claim: give.fullClaim,
claimType: "GiveAction", claimType: "GiveAction",
@ -779,7 +781,7 @@ export default class ProjectViewView extends Vue {
} }
// similar code is found in ClaimView // similar code is found in ClaimView
async confirmClaim(give: GiveServerRecord) { async confirmClaim(give: GiveSummaryRecord) {
if (confirm("Do you personally confirm that this is true?")) { if (confirm("Do you personally confirm that this is true?")) {
// similar logic is found in endorser-mobile // similar logic is found in endorser-mobile
const goodClaim = serverUtil.removeSchemaContext( const goodClaim = serverUtil.removeSchemaContext(

6
src/views/ProjectsView.vue

@ -167,7 +167,7 @@
<a @click="onClickLoadClaim(offer.jwtId)"> <a @click="onClickLoadClaim(offer.jwtId)">
<fa <fa
icon="circle-info" icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
></fa> ></fa>
</a> </a>
@ -223,7 +223,7 @@ import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.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 { OfferServerRecord, PlanData } from "@/libs/endorserServer"; import { OfferSummaryRecord, PlanData } from "@/libs/endorserServer";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@Component({ @Component({
@ -237,7 +237,7 @@ export default class ProjectsView extends Vue {
currentIid: IIdentifier; currentIid: IIdentifier;
isLoading = false; isLoading = false;
numAccounts = 0; numAccounts = 0;
offers: OfferServerRecord[] = []; offers: OfferSummaryRecord[] = [];
showOffers = true; showOffers = true;
showProjects = false; showProjects = false;

8
src/views/QuickActionBvcEndView.vue

@ -60,7 +60,7 @@
}} }}
<a @click="onClickLoadClaim(record.id)"> <a @click="onClickLoadClaim(record.id)">
<fa <fa
icon="circle-info" icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
/> />
</a> </a>
@ -146,7 +146,7 @@ import {
createAndSubmitConfirmation, createAndSubmitConfirmation,
createAndSubmitGive, createAndSubmitGive,
ErrorResult, ErrorResult,
GenericServerRecord, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
@ -166,7 +166,7 @@ export default class QuickActionBvcBeginView extends Vue {
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
claimCountWithHidden = 0; claimCountWithHidden = 0;
claimsToConfirm: GenericServerRecord[] = []; claimsToConfirm: GenericCredWrapper[] = [];
claimsToConfirmSelected: string[] = []; claimsToConfirmSelected: string[] = [];
description = "breakfast"; description = "breakfast";
loadingConfirms = true; loadingConfirms = true;
@ -228,7 +228,7 @@ export default class QuickActionBvcBeginView extends Vue {
} }
await response.json().then((data) => { await response.json().then((data) => {
const dataByOthers = R.reject( const dataByOthers = R.reject(
(claim: GenericServerRecord) => claim.issuer === this.activeDid, (claim: GenericCredWrapper) => claim.issuer === this.activeDid,
data, data,
); );
const dataByOthersWithoutHidden = R.reject( const dataByOthersWithoutHidden = R.reject(

8
src/views/SearchAreaView.vue

@ -64,10 +64,11 @@
</div> </div>
</div> </div>
<div style="height: 600px; width: 800px"> <div class="mb-4 aspect-video">
<l-map <l-map
ref="map" ref="map"
:center="[localCenterLat, localCenterLong]" :center="[localCenterLat, localCenterLong]"
class="!z-40 rounded-md"
v-model:zoom="localZoom" v-model:zoom="localZoom"
@click="setMapPoint" @click="setMapPoint"
> >
@ -208,9 +209,9 @@ export default class DiscoverView extends Vue {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Saved", title: "Saved",
text: "That has been saved in your preferences.", text: "That has been saved in your preferences. You can now filter by it on your home screen feed.",
}, },
-1, 7000,
); );
this.$router.back(); this.$router.back();
} catch (err) { } catch (err) {
@ -246,6 +247,7 @@ export default class DiscoverView extends Vue {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [], searchBoxes: [],
filterFeedByNearby: false,
}); });
this.searchBox = null; this.searchBox = null;
this.localCenterLat = 0; this.localCenterLat = 0;

64
src/views/StartView.vue

@ -23,36 +23,40 @@
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
<div id="start-question" class="mt-8"> <div id="start-question" class="mt-8">
<p class="text-center text-xl font-light"> <div class="max-w-3xl mx-auto">
Do you want a new identifier of your own? <p class="text-center text-xl font-light">
</p> Do you want a new identifier of your own?
<p class="text-center font-light"> </p>
If you haven't used this before, click "Yes" to generate a new <p class="text-center font-light">
identifier. If you haven't used this before, click "Yes" to generate a new
</p> identifier.
<p class="text-center mb-4 font-light"> </p>
Only click "No" if you have a seed of 12 or 24 words generated <p class="text-center mb-4 font-light">
elsewhere. Only click "No" if you have a seed of 12 or 24 words generated
</p> elsewhere.
<a </p>
@click="onClickYes()" <a
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" @click="onClickYes()"
> 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 mb-2"
Yes, generate one >
</a> Yes, generate one
<a </a>
@click="onClickNo()" <div class="grid grid-cols-1 sm:grid-cols-2 gap-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" <a
> @click="onClickNo()"
No, I have a seed 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"
</a> >
<a No, I have a seed
v-if="numAccounts > 0" </a>
@click="onClickDerive()" <a
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" v-if="numAccounts > 0"
> @click="onClickDerive()"
Derive new address from existing seed 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"
</a> >
Derive new address from existing seed
</a>
</div>
</div>
</div> </div>
</section> </section>
</template> </template>

74
tsconfig.json

@ -1,47 +1,35 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowJs": true, "target": "ES2020", // Latest ECMAScript features that are widely supported by modern browsers
"resolveJsonModule": true, "module": "ESNext", // Use ES modules
"target": "esnext", "strict": true, // Enable all strict type checking options
"module": "esnext", "jsx": "preserve", // Preserves JSX to be transformed by Babel or another transpiler
"strict": true, "moduleResolution": "node", // Use Node.js style module resolution
"strictPropertyInitialization": false, "experimentalDecorators": true,
"jsx": "preserve", "esModuleInterop": true, // Enables compatibility with CommonJS modules for default imports
"moduleResolution": "node", "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
"experimentalDecorators": true, "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file
"skipLibCheck": true, "useDefineForClassFields": true,
"esModuleInterop": true, "sourceMap": true,
"allowSyntheticDefaultImports": true, "baseUrl": "./src", // Base directory to resolve non-relative module names
"forceConsistentCasingInFileNames": true, "paths": {
"useDefineForClassFields": true, "@/components/*": ["components/*"],
"sourceMap": true, "@/views/*": ["views/*"],
"baseUrl": "./src", "@/db/*": ["db/*"],
"types": [ "@/libs/*": ["libs/*"],
"webpack-env" "@/constants/*": ["constants/*"],
], "@/store/*": ["store/*"]
"paths": { },
"@/components/*": ["components/*"], "lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs
"@/views/*": ["views/*"],
"@/db/*": ["db/*"],
"@/libs/*": ["libs/*"],
"@/constants/*": ["constants/*"],
"@/store/*": ["store/*"],
}, },
"lib": [ "include": [
"esnext", "src/**/*.ts",
"dom", "src/**/*.tsx",
"dom.iterable", "src/**/*.vue",
"scripthost" "tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
] ]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
} }

18
vite.config.mjs

@ -0,0 +1,18 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import * as path from "path";
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 8080
},
plugins: [ vue() ],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
buffer: path.resolve(__dirname, 'node_modules', 'buffer'),
'dexie-export-import/dist/import': 'dexie-export-import/dist/import/index.js',
},
},
});

4
vue.config.js

@ -2,9 +2,9 @@ const { defineConfig } = require("@vue/cli-service");
const { gitDescribeSync } = require("git-describe"); const { gitDescribeSync } = require("git-describe");
const { exec } = require("child_process"); const { exec } = require("child_process");
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash; import.meta.env.VITE_GIT_HASH = gitDescribeSync().hash;
const TIME_SAFARI_APP_TITLE = const TIME_SAFARI_APP_TITLE =
process.env.TIME_SAFARI_APP_TITLE || require("./package.json").name; import.meta.env.VITE_TIME_SAFARI_APP_TITLE || require("./package.json").name;
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true, transpileDependencies: true,

Loading…
Cancel
Save