Compare commits

...

103 Commits

Author SHA1 Message Date
Matthew Raymer
80a1185faa Fix for missing scene-container 2023-07-05 15:59:58 +08:00
d98e95915b Merge pull request 'error-logging' (#29) from error-logging into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#29
2023-07-04 12:15:24 -04:00
Matthew Raymer
0a020a4069 Clean up vestigle old alert code 2023-07-04 19:29:12 +08:00
Matthew Raymer
c859778832 Add AlertMessage component 2023-07-04 19:27:57 +08:00
Matthew Raymer
c24022c41c Make the visibility of the Alert indirect 2023-07-04 19:26:05 +08:00
Matthew Raymer
0fd4b86a84 A bit of linting 2023-07-03 21:06:24 +08:00
Matthew Raymer
c31445865e Straggler 2023-07-03 21:05:45 +08:00
Matthew Raymer
0af03227a6 Propagated AlertMessage component 2023-07-03 21:04:53 +08:00
Matthew Raymer
3c977a1f28 First roll out of an AlertMessage control 2023-07-03 20:24:23 +08:00
Matthew Raymer
8d8635a3e6 Added try...catch and linted 2023-07-03 19:44:41 +08:00
Matthew Raymer
bcc6de6fc0 New AlertMessage component 2023-07-03 19:40:53 +08:00
Matthew Raymer
99ea161da0 Removed created() and relocated to mounted() could not replicate issue 2023-07-03 18:48:01 +08:00
Matthew Raymer
3f6dbdebef Moved checkLimits inside try...catch since it has an identity check 2023-07-03 18:46:23 +08:00
Matthew Raymer
b139957e3e Added identity check 2023-07-03 18:44:42 +08:00
6e4f6d090a Merge pull request 'add project search (which just goes to console for now)' (#28) from search-projects into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#28
2023-07-02 20:21:35 -04:00
48227e8cf2 Merge branch 'master' into search-projects 2023-07-02 20:21:19 -04:00
09f02ca4b2 Merge pull request 'Quick links to record a gift, to this person or to a project' (#27) from give-to-project into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#27
2023-07-02 20:20:51 -04:00
9b3823ef0e Merge branch 'master' into give-to-project 2023-07-02 20:06:11 -04:00
cdeece1795 add project search (which just goes to console for now) 2023-07-02 07:55:45 -06:00
c2ebaa0a76 mark the give-to-project task as complete 2023-07-02 06:46:31 -06:00
3f60051599 Merge branch 'master' into give-to-project 2023-07-01 22:33:23 -06:00
a8f1e25986 add error messages for gives 2023-07-01 22:20:10 -06:00
964248e895 Update 'project.task.yaml' 2023-07-01 21:04:06 -04:00
a2b3cebdb3 finish contact selection for gives 2023-07-01 15:45:30 -06:00
bc6e52774c update tasks 2023-07-01 07:13:39 -06:00
643f777d10 start linking gives to projects 2023-07-01 07:07:46 -06:00
ec1d8404ca Merge pull request 'start a walk-through of the test' (#26) from scenarios into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#26
2023-07-01 08:12:55 -04:00
Jose Olarte III
1d6241abbb Various changes
- Loading animation in Projects view
- Per item icon + spacing fixes in Home view
2023-07-01 16:04:32 +08:00
c40b690878 fix load-project function 2023-06-28 20:28:14 -06:00
c9c81f1e5c fix wording in list of contacts, remove console.log 2023-06-28 20:20:06 -06:00
a94069e70a update tasks from previous conversation, marking milestone:2 done 2023-06-27 20:54:07 -06:00
Matthew Raymer
53f42e1ad3 Updates to fix merge and remove console.log 2023-06-27 20:07:38 +08:00
Matthew Raymer
5f0bbccbe6 Merge branch 'infinitescroll-test' 2023-06-27 19:06:41 +08:00
Matthew Raymer
3ec9056901 Merging remote master into local 2023-06-27 19:06:25 +08:00
Matthew Raymer
6d3ab7c313 Added loading settings (no image yet) plus refactored types a little more. 2023-06-27 18:57:36 +08:00
d9e9a7b740 start a walk-through 2023-06-26 20:00:23 -06:00
ea95382fdf change jwtId to rowid for paging within a derivative/cached table 2023-06-26 19:20:02 -06:00
072b663ec9 change comment whitespace to get past lint errors 2023-06-26 19:18:38 -06:00
Matthew Raymer
6393a20e7e Documentation added 2023-06-26 20:45:13 +08:00
Matthew Raymer
19d934eb28 Do a merge 2023-06-26 20:02:56 +08:00
Matthew Raymer
49ce7d43b0 Refactored and streamlined code. Still having an issue with beforeId? 2023-06-26 19:53:28 +08:00
Matthew Aaron Raymer
6233189a49 Merging next-iteration 2023-06-26 13:59:47 +08:00
Matthew Aaron Raymer
ffb0f2d37f Merge branch 'master' of ssh://173.199.124.46:222/trent_larson/kick-starter-for-time-pwa into allow-new 2023-06-26 13:49:59 +08:00
502352ad36 Update 'project.task.yaml' 2023-06-25 21:08:17 -04:00
db7b3fff06 mark another task off (no code changes) 2023-06-25 17:22:47 -06:00
bd75802a0c mark another task fixed (no code changes) 2023-06-25 17:22:06 -06:00
1a86730354 make display on creation page look halfway decent, and switch fully to it 2023-06-25 17:20:22 -06:00
a96728bec5 add better messaging on registration failure 2023-06-25 16:41:32 -06:00
944b0ad759 fix registration, separate ID creation to allow new random ones, and refactor warning and other verbiage 2023-06-24 21:07:21 -06:00
42bf34f549 fix infinitescroll to work off correct ID 2023-06-24 06:45:03 -06:00
Matthew Raymer
071c41b70c Firing properly but no data returned on extended fetch 2023-06-24 17:47:18 +08:00
5747404fd6 add marker in feed to show where they've seen claims, plus other small clean-up 2023-06-23 20:17:54 -06:00
639f630436 remove old texture image that's been replaced 2023-06-23 17:37:09 -06:00
a8794be2ea sllow quick gifting all the way to the server, maybe with hours 2023-06-23 17:00:20 -06:00
Matthew Raymer
0726a8d3ba New modules and InfiniteScroll init 2023-06-23 18:56:26 +08:00
aa2f484a9f add the other activity envisioned on the home page (though not sending data yet) 2023-06-22 20:51:06 -06:00
07e7a70d56 in feed: add token for authorized request, plus better descriptions 2023-06-12 20:29:55 -06:00
6daa515d19 load feed of give records on home screen 2023-06-11 20:37:34 -06:00
d5336dbf1b docs & comments & labels 2023-06-11 17:02:20 -06:00
b0fc8818ee add timing & animation details to stats-world 2023-05-28 09:11:37 -06:00
9f49234179 update help instructions for importing another identity 2023-05-28 07:21:35 -06:00
32351b07b7 prefer console.error messages for errors 2023-05-28 07:10:00 -06:00
0ce06bd9ac fix a problem with test data (divide by 0), and map full addresses to points 2023-05-27 17:48:41 -06:00
c3c16fd15b Merge pull request 'add QR code for contact info' (#22) from contact-qr into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#22
2023-05-22 17:27:50 -04:00
a16c34d4ee update project tasks 2023-05-21 06:59:48 -06:00
40f9de0609 add QR code for contact info 2023-05-20 20:41:15 -06:00
d1194297ac Merge branch 'switching-servers' 2023-05-20 17:34:22 -06:00
6d67a3e8e5 Merge branch '3d-world' 2023-05-20 17:19:27 -06:00
b0ccd84b62 Merge pull request 'add page to show mnemonic seed phrase for backup' (#20) from seed-backup into switching-servers
Reviewed-on: trent_larson/kick-starter-for-time-pwa#20
2023-05-20 00:47:22 -04:00
b94e36ef3b Merge pull request 'allow to switch the server' (#19) from switching-servers into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#19
2023-05-20 00:47:11 -04:00
0fed104be1 Merge branch 'master' into 3d-world 2023-05-20 00:46:30 -04:00
aa34a2362f update tasks (and rename file so the format is obvious) 2023-05-19 16:40:41 -06:00
55b53955fc add page to show mnemonic seed phrase for backup 2023-05-19 15:49:34 -06:00
fc7c1187e8 allow to switch the server 2023-05-19 13:28:52 -06:00
2feea0d645 remove console.log 2023-05-19 08:11:34 -06:00
8f3a11bb98 in stats-world, fix the location computations to be based on Give attributes 2023-05-18 20:34:57 -06:00
beb7821f58 in stats-world, refactor claim-loading code to separate file (no logic changes) 2023-05-18 18:54:11 -06:00
712b25bc71 add error message to stats page 2023-05-18 18:23:17 -06:00
f039f98b61 on stats-world: adjust some lighting and position, and add an attempt at a screen-capture 2023-05-18 18:04:10 -06:00
f7a149444a tweak timing & spacing of stats-world, and log relevant tasks 2023-05-17 21:13:01 -06:00
58e962a3bd load stats-world bushes and make 'em grow! 2023-05-17 20:46:40 -06:00
7160aa3cc5 remove test cube from stats-world 2023-05-17 18:22:46 -06:00
786f0bd94a make the lights wait to turn on in stats-world 2023-05-17 18:20:55 -06:00
b5db2b4140 refactor variable names (no logic changes) 2023-05-17 17:42:26 -06:00
faa7959929 make all lights move in stats-world 2023-05-17 17:40:10 -06:00
3dd1b6f6f0 add some other screen actions (kinda ugly); add load of claims 2023-05-17 08:46:10 -06:00
30b8b941ae Merge pull request 'copy message & infinite scroll start' (#17) from hack-copy into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#17
2023-05-17 02:32:12 -04:00
9b1c51ba15 adjust stats-world perspective & size & colors 2023-05-16 18:56:50 -06:00
8c6c32ed20 finish removal from last commit 2023-05-16 16:40:55 -06:00
0eaf72b83b remove yet more randomness that has no effect in stats-world 2023-05-16 16:30:33 -06:00
41d3ad56f5 remove some random numbers that don't seem to make a difference in stats-world 2023-05-16 16:26:27 -06:00
0227d32f15 add a simple box in stats-world 2023-05-16 16:25:32 -06:00
b5ab485354 enable zooming on the stats-world 2023-05-16 16:17:34 -06:00
02ae78de7b totally remove code to make terrain bob up and down 2023-05-16 07:50:26 -06:00
64f3dbd138 play with sizing, and stop bobbing and rotation 2023-05-16 07:49:44 -06:00
f603882d42 modify some things to remove warnings on server and in browser console 2023-05-16 06:39:36 -06:00
a9844e6e78 add beginning of visualization for statistics, unmodified from blog code 2023-05-16 06:22:19 -06:00
e4f3f9b2e0 remove extra logging 2023-05-15 16:45:22 -06:00
d7d53a5b8c refactor & shorten the 'copied' message display logic 2023-05-14 20:38:27 -06:00
44ed39b5c1 eliminate extra code for quick-message-display 2023-05-12 21:40:10 -06:00
Matthew Aaron Raymer
0dbc018c8d A bit of infinity and hack for copy message 2023-05-13 11:19:53 +08:00
fb7d51ac4c Merge pull request 'add help blurb for when we don't see info about someone that we should see' (#16) from help-visibility into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#16
2023-05-11 21:21:44 -04:00
85031f84c0 add help blurb for when we don't see info about someone that we should see 2023-05-07 19:52:40 -06:00
49 changed files with 5908 additions and 3491 deletions

View File

@@ -20,11 +20,6 @@ npm run build
npm run lint
```
### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start
(because it'll generate a new one automatically if you start on the `/account` page).
### Test key contents
See [this page](openssl_signing_console.rst)
@@ -59,17 +54,35 @@ playing one of two ways:
### Create multiple identifiers
Go to /import-account and import a new one. Then switch identifiers on the
bottom of the Your Identity page.
Go to /start and create or import a new one. Then switch identifiers on the bottom of the Your Identity page.
### Create keys with alternate tools
See [this page](openssl_signing_console.rst)
### Customize configuration
### Customize Vue configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Scenarios
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start
(because it'll generate a new one automatically if you start on the `/account` page).
## Dependencies
See https://tea.xyz
@@ -81,6 +94,8 @@ See https://tea.xyz
## Other
### Reference Material
```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
@@ -183,3 +198,12 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, [])
}
```
## Kudos
Gifts make the world go 'round!
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)

5583
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,50 +12,54 @@
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@pvermeer/dexie-encrypted-addon": "^2.0.5",
"@veramo/core": "^5.1.2",
"@veramo/credential-w3c": "^5.1.4",
"@veramo/data-store": "^5.1.2",
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0",
"@veramo/core": "^5.2.0",
"@veramo/credential-w3c": "^5.2.0",
"@veramo/data-store": "^5.2.0",
"@veramo/did-manager": "^5.1.2",
"@veramo/did-provider-ethr": "^5.1.2",
"@veramo/did-resolver": "^5.1.2",
"@veramo/did-resolver": "^5.2.0",
"@veramo/key-manager": "^5.1.2",
"@vueuse/core": "^9.13.0",
"@vueuse/core": "^10.2.0",
"@zxing/text-encoding": "^0.9.0",
"axios": "^1.3.4",
"axios": "^1.4.0",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1",
"core-js": "^3.29.1",
"dexie": "^3.2.3",
"core-js": "^3.31.0",
"dexie": "^3.2.4",
"dexie-export-import": "^4.0.7",
"did-jwt": "^6.11.5",
"ethereum-cryptography": "^1.2.0",
"did-jwt": "^7.2.2",
"ethereum-cryptography": "^2.0.0",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0",
"js-generate-password": "^0.1.7",
"js-generate-password": "^0.1.9",
"localstorage-slim": "^2.4.0",
"luxon": "^3.3.0",
"merkletreejs": "^0.3.9",
"merkletreejs": "^0.3.10",
"moment": "^2.29.4",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0",
"ramda": "^0.28.0",
"readable-stream": "^4.3.0",
"qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.0",
"readable-stream": "^4.4.0",
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2",
"vue": "^3.2.47",
"three": "^0.153.0",
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-class-component": "^8.0.0-0",
"vue-facing-decorator": "^2.1.19",
"vue-facing-decorator": "^2.1.20",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.1.6",
"web-did-resolver": "^2.0.22"
"vue-router": "^4.2.2",
"web-did-resolver": "^2.0.24"
},
"devDependencies": {
"@types/ramda": "^0.28.23",
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"@types/ramda": "^0.29.2",
"@types/three": "^0.152.1",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8",
@@ -63,15 +67,15 @@
"@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.2",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.37.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.10.0",
"postcss": "^8.4.21",
"prettier": "^2.8.7",
"tailwindcss": "^3.3.1",
"typescript": "~5.0.3"
"eslint-plugin-vue": "^9.15.0",
"postcss": "^8.4.24",
"prettier": "^2.8.8",
"tailwindcss": "^3.3.2",
"typescript": "~5.1.3"
}
}

115
project.task.yaml Normal file
View File

@@ -0,0 +1,115 @@
tasks:
- replace user-affecting console.log & console.error with error messages (eg. catches)
- if there's no identity, handle it on pages which expect an identity (eg. project -- look for JSON.parse identity calls)
- .1 show an appropriate message when there are no contacts
- 8 Move to vue-facing-decorator
- 01 design ideas for simple gives on the first page
- .1 remove commitments from ProjectView UI
- 01 add list of 'give' records for a project on ProjectView UI
- 02 Discover page - display results (currently in console.log), spin when searching
- 08 search by location, endpoint, etc assignee:trent
- 01 add a location for a project via map pin :
- give attribute to use assignee:trent
- 01 remove all the "form" fields (or at least investigate to see if that page refresh is desired)
- 08 Scan QR code to import into contacts.
- contacts v1 :
- 01 Import contact info a la QR code.
- 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 move all "identity" references to temporary account access assignee:trent
- contacts v+ :
- .2 show error to user when adding a duplicate contact
- 01 parse input more robustly (with CSV lib and not commas)
- refactor UI :
- .5 Alerts show at the top and can be missed, eg. account data download
- 01 Change alert popup code a component (to cut down duplicate code; see "in many files")
- 01 Change "nav" tabs across the bottom into a component (eliminating duplicate code).
- .5 Fix how icons show on top of bottom bar on ContactAmounts page
- .2 Hide "Advanced" section in Account page by default
- show pop-up confirming that settings & contacts have been downloaded
- Ensure each action sent to the server has a confirmation - registration
- Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery")
- 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
- .5 customize favicon
- .2 Hide "Advanced" section in Account page by default
- 04 allow user to download claims, mine + ones I can see about me from others
- 24 Move to Vite
- 40 notifications :
- push
- Discuss whether the remaining tasks are worthwhile before MVP release.
- 01 fix images on project page, on discovery page
- .2 fix "Rotary" and static icon to the right on project page
- stats v1 :
- 01 show numeric stats
- 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- 4-8 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- Do we want split first name & last name?
- remove 'about' page
- Release Minimum Viable Product :
- 08 thorough testing for errors & edge cases
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Rename DB to TimeSafari.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- 40 notifications v+ :
- pull, w/ scheduled runs
- linking between projects or plans :
- terminology:
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
- Stats :
- 01 point out user's location on the world
- 01 present a credential selected from the stats
- 04 show gives spreading to other places
- badge for most gives/receives/confirms per day/week/month
- badge for amount given/offered to your project
- set a goal of given/offers
- automated tests, eg. cypress
- Notifications (wake on the phone, push notifications)
- Connect with phone contacts
- Multiple identities
- Peer DID
- DIDComm
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27

View File

@@ -1,46 +0,0 @@
- top screens from img/screens.pdf milestone:2 :
- view all :
- add infinite scroll assignee:matthew
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- replace user-affecting console.logs with error messages (eg. catches)
- contacts v1 :
- produce a video assignee:trent
- .2 warn about amounts when you cannot see them
- .1 remove 'copy' until it works
- .5 switch to prod server
- .5 Add page to show seed.
- 01 Provide a way to import the non-sensitive data.
- 01 Provide way to share your contact info.
- .2 move all "identity" references to temporary account access
- .5 make deploy for give-only features
- .5 get 'copy' to work on account page
- contacts v+ :
- .5 make advanced "show/hide amounts" button into a nice UI toggle
- .2 show error to user when adding a duplicate contact
- parse input more robustly (with CSV lib and not commas)
- refactor UI :
- .5 Alerts show at the top and can be missed, eg. account data download
- 01 Change alerts into a component (to cut down duplicate code)
- 01 Code for "nav" tabs across the bottom is duplicated on each page.
- .2 Add "copied" feedback when they click "copy" on /account
- .5 Fix how icons show on top of bottom bar on ContactAmounts page
- commit screen
- discover screen
- backup all data
- Next Viable Product afterward
- Connect with phone contacts
- Multiple identities
- Peer DID
- DIDComm

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

View File

@@ -0,0 +1,11 @@
Model Information:
* title: Lupine Plant
* source: https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439
* author: rufusrockwell (https://sketchfab.com/rufusrockwell)
Model License:
* license type: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
* requirements: Author must be credited. Commercial use is allowed.
If you use this 3D model in your project be sure to copy paste this credit wherever you share it:
This work is based on "Lupine Plant" (https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) by rufusrockwell (https://sketchfab.com/rufusrockwell) licensed under CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)

Binary file not shown.

View File

@@ -0,0 +1,229 @@
{
"accessors": [
{
"bufferView": 2,
"componentType": 5126,
"count": 2759,
"max": [
41.3074951171875,
40.37548828125,
87.85917663574219
],
"min": [
-35.245540618896484,
-36.895416259765625,
-0.9094290137290955
],
"type": "VEC3"
},
{
"bufferView": 2,
"byteOffset": 33108,
"componentType": 5126,
"count": 2759,
"max": [
0.9999382495880127,
0.9986748695373535,
0.9985831379890442
],
"min": [
-0.9998949766159058,
-0.9975876212120056,
-0.411094069480896
],
"type": "VEC3"
},
{
"bufferView": 3,
"componentType": 5126,
"count": 2759,
"max": [
0.9987699389457703,
0.9998998045921326,
0.9577858448028564,
1.0
],
"min": [
-0.9987726807594299,
-0.9990445971488953,
-0.999801516532898,
1.0
],
"type": "VEC4"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 2759,
"max": [
1.0061479806900024,
0.9993550181388855
],
"min": [
0.00279300007969141,
0.0011620000004768372
],
"type": "VEC2"
},
{
"bufferView": 0,
"componentType": 5125,
"count": 6378,
"type": "SCALAR"
}
],
"asset": {
"extras": {
"author": "rufusrockwell (https://sketchfab.com/rufusrockwell)",
"license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)",
"source": "https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439",
"title": "Lupine Plant"
},
"generator": "Sketchfab-12.68.0",
"version": "2.0"
},
"bufferViews": [
{
"buffer": 0,
"byteLength": 25512,
"name": "floatBufferViews",
"target": 34963
},
{
"buffer": 0,
"byteLength": 22072,
"byteOffset": 25512,
"byteStride": 8,
"name": "floatBufferViews",
"target": 34962
},
{
"buffer": 0,
"byteLength": 66216,
"byteOffset": 47584,
"byteStride": 12,
"name": "floatBufferViews",
"target": 34962
},
{
"buffer": 0,
"byteLength": 44144,
"byteOffset": 113800,
"byteStride": 16,
"name": "floatBufferViews",
"target": 34962
}
],
"buffers": [
{
"byteLength": 157944,
"uri": "scene.bin"
}
],
"images": [
{
"uri": "textures/lambert2SG_baseColor.png"
},
{
"uri": "textures/lambert2SG_normal.png"
}
],
"materials": [
{
"alphaCutoff": 0.2,
"alphaMode": "MASK",
"doubleSided": true,
"name": "lambert2SG",
"normalTexture": {
"index": 1
},
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 0
},
"metallicFactor": 0.0
}
}
],
"meshes": [
{
"name": "Object_0",
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 0,
"TANGENT": 2,
"TEXCOORD_0": 3
},
"indices": 4,
"material": 0,
"mode": 4
}
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
2.220446049250313e-16,
-1.0,
0.0,
0.0,
1.0,
2.220446049250313e-16,
0.0,
0.0,
0.0,
0.0,
1.0
],
"name": "Sketchfab_model"
},
{
"children": [
2
],
"name": "LupineSF.obj.cleaner.materialmerger.gles"
},
{
"mesh": 0,
"name": "Object_2"
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9987,
"wrapS": 10497,
"wrapT": 10497
}
],
"scene": 0,
"scenes": [
{
"name": "Sketchfab_Scene",
"nodes": [
0
]
}
],
"textures": [
{
"sampler": 0,
"source": 0
},
{
"sampler": 0,
"source": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

@@ -0,0 +1,47 @@
<template>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
@Component
export default class AlertMessage extends Vue {
@Prop alertTitle = "";
@Prop alertMessage = "";
isAlertVisible = this.alertMessage;
public onClickClose() {
this.isAlertVisible = false;
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

View File

@@ -0,0 +1,108 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-lg text-center">
{{ message }} {{ giver?.name || "somebody not specified" }}
</h1>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
placeholder="What was received"
v-model="description"
/>
<div class="flex flex-row">
<span class="py-4">Hours</span>
<input
type="text"
class="block w-8 rounded border border-slate-400 ml-4 text-center"
v-model="hours"
/>
<div class="flex flex-col px-1">
<div>
<fa icon="square-caret-up" size="2xl" @click="increment()" />
</div>
<div>
<fa icon="square-caret-down" size="2xl" @click="decrement()" />
</div>
</div>
</div>
<p class="text-right">Sign & Send to publish to the world</p>
<div class="text-right">
<button class="rounded border border-slate-400" @click="confirm">
<span class="m-2">Sign & Send</span>
</button>
&nbsp;
<button class="rounded border border-slate-400" @click="cancel">
<span class="m-2">Cancel</span>
</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["message"],
data() {
return {
giver: null,
description: "",
hours: "0",
visible: false,
};
},
methods: {
open(giver) {
// giver: GiverInputInfo
this.giver = giver;
this.visible = true;
},
close() {
this.visible = false;
},
increment() {
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
},
decrement() {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
},
confirm() {
this.close();
this.$emit("dialog-result", {
action: "confirm",
giver: this.giver,
hours: parseFloat(this.hours),
description: this.description,
});
this.description = "";
this.giver = null;
this.hours = "0";
},
cancel() {
this.close();
this.$emit("dialog-result", { action: "cancel" });
},
},
};
</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;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 50%;
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div ref="scrollContainer">
<slot />
<div ref="sentinel" style="height: 1px"></div>
</div>
</template>
<script lang="ts">
import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
@Component
export default class InfiniteScroll extends Vue {
@Prop({ default: 200 })
readonly distance!: number;
private observer!: IntersectionObserver;
private isInitialRender = true;
updated() {
if (!this.observer) {
const options = {
root: null,
rootMargin: `0px 0px ${this.distance}px 0px`,
threshold: 1.0,
};
this.observer = new IntersectionObserver(
this.handleIntersection,
options
);
this.observer.observe(this.$refs.sentinel as HTMLElement);
}
}
// 'beforeUnmount' hook runs before unmounting the component
beforeUnmount() {
if (this.observer) {
this.observer.disconnect();
}
}
@Emit("reached-bottom")
handleIntersection(entries: IntersectionObserverEntry[]) {
const entry = entries[0];
if (entry.isIntersecting) {
return true;
}
return false;
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

View File

@@ -0,0 +1,110 @@
// from https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80
import * as TWEEN from "@tweenjs/tween.js";
import * as THREE from "three";
import { createCamera } from "./components/camera.js";
import { createLights } from "./components/lights.js";
import { createScene } from "./components/scene.js";
import { loadLandmarks } from "./components/objects/landmarks.js";
import { createTerrain } from "./components/objects/terrain.js";
import { Loop } from "./systems/Loop.js";
import { Resizer } from "./systems/Resizer.js";
import { createControls } from "./systems/controls.js";
import { createRenderer } from "./systems/renderer.js";
const COLOR1 = "#dddddd";
const COLOR2 = "#0055aa";
class World {
constructor(container, vue) {
this.PLATFORM_BORDER = 5;
this.PLATFORM_EDGE_FOR_UNKNOWNS = 10;
this.PLATFORM_SIZE = 100; // note that the loadLandmarks calculations may still assume 100
this.update = this.update.bind(this);
this.vue = vue;
// Instances of camera, scene, and renderer
this.camera = createCamera();
this.scene = createScene(COLOR2);
this.renderer = createRenderer();
// necessary for models, says https://threejs.org/docs/index.html#examples/en/loaders/GLTFLoader
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
this.light = null;
this.lights = [];
this.bushes = [];
// Initialize Loop
this.loop = new Loop(this.camera, this.scene, this.renderer);
container.append(this.renderer.domElement);
// Orbit Controls
const controls = createControls(this.camera, this.renderer.domElement);
// Light Instance, with optional light helper
const { light } = createLights(COLOR1);
// Terrain Instance
const terrain = createTerrain({
color: COLOR1,
height: this.PLATFORM_SIZE + this.PLATFORM_BORDER * 2,
width:
this.PLATFORM_SIZE +
this.PLATFORM_BORDER * 2 +
this.PLATFORM_EDGE_FOR_UNKNOWNS * 2,
});
this.loop.updatables.push(controls);
this.loop.updatables.push(light);
this.loop.updatables.push(terrain);
this.scene.add(light, terrain);
loadLandmarks(vue, this, this.scene, this.loop);
requestAnimationFrame(this.update);
// Responsive handler
const resizer = new Resizer(container, this.camera, this.renderer);
resizer.onResize = () => {
this.render();
};
}
update(time) {
TWEEN.update(time);
this.lights.forEach((light) => {
light.updateMatrixWorld();
light.target.updateMatrixWorld();
});
this.lights.forEach((bush) => {
bush.updateMatrixWorld();
});
requestAnimationFrame(this.update);
}
render() {
// draw a single frame
this.renderer.render(this.scene, this.camera);
}
// Animation handlers
start() {
this.loop.start();
}
stop() {
this.loop.stop();
}
setExposedWorldProperties(key, value) {
this.vue.setWorldProperty(key, value);
}
}
export { World };

View File

@@ -0,0 +1,19 @@
import { PerspectiveCamera } from "three";
function createCamera() {
const camera = new PerspectiveCamera(
35, // fov = Field Of View
1, // aspect ratio (dummy value)
0.1, // near clipping plane
350 // far clipping plane
);
// move the camera back so we can view the scene
camera.position.set(0, 100, 200);
// eslint-disable-next-line @typescript-eslint/no-empty-function
camera.tick = () => {};
return camera;
}
export { createCamera };

View File

@@ -0,0 +1,14 @@
import { DirectionalLight, DirectionalLightHelper } from "three";
function createLights(color) {
const light = new DirectionalLight(color, 4);
const lightHelper = new DirectionalLightHelper(light, 0);
light.position.set(60, 100, 30);
// eslint-disable-next-line @typescript-eslint/no-empty-function
light.tick = () => {};
return { light, lightHelper };
}
export { createLights };

View File

@@ -0,0 +1,251 @@
import axios from "axios";
import * as R from "ramda";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
const ANIMATION_DURATION_SECS = 10;
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
export async function loadLandmarks(vue, world, scene, loop) {
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer;
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await axios.get(url, { headers: headers });
if (resp.status === 200) {
const landmarks = resp.data.data;
const minDate = landmarks[landmarks.length - 1].issuedAt;
const maxDate = landmarks[0].issuedAt;
world.setExposedWorldProperties("startTime", minDate.replace("T", " "));
world.setExposedWorldProperties("endTime", maxDate.replace("T", " "));
const minTimeMillis = new Date(minDate).getTime();
const fullTimeMillis =
maxDate > minDate ? new Date(maxDate).getTime() - minTimeMillis : 1; // avoid divide by zero
// ratio of animation time to real time
const fakeRealRatio = (ANIMATION_DURATION_SECS * 1000) / fullTimeMillis;
// load plant model first because it takes a second
const loader = new GLTFLoader();
// choose the right plant
const modelLoc = "/models/lupine_plant/scene.gltf", // push with pokies
modScale = 0.1;
//const modelLoc = "/models/round_bush/scene.gltf", // green & pink
// modScale = 1;
//const modelLoc = "/models/coreopsis-flower.glb", // 3 flowers
// modScale = 2;
//const modelLoc = "/models/a_bush/scene.gltf", // purple leaves
// modScale = 15;
// calculate positions for each claim, especially because some are random
const locations = landmarks.map((claim) =>
locForGive(claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS)
);
// eslint-disable-next-line @typescript-eslint/no-this-alias
loader.load(
modelLoc,
function (gltf) {
gltf.scene.scale.set(0, 0, 0);
for (let i = 0; i < landmarks.length; i++) {
// claim is a GiveServerRecord (see endorserServer.ts)
const claim = landmarks[i];
const newPlant = SkeletonUtils.clone(gltf.scene);
const loc = locations[i];
newPlant.position.set(loc.x, 0, loc.z);
world.scene.add(newPlant);
const timeDelayMillis =
fakeRealRatio *
(new Date(claim.issuedAt).getTime() - minTimeMillis);
new TWEEN.Tween(newPlant.scale)
.delay(timeDelayMillis)
.to({ x: modScale, y: modScale, z: modScale }, 5000)
.start();
world.bushes = [...world.bushes, newPlant];
}
},
undefined,
function (error) {
console.error(error);
}
);
// calculate when lights shine on appearing claim area
for (let i = 0; i < landmarks.length; i++) {
// claim is a GiveServerRecord (see endorserServer.ts)
const claim = landmarks[i];
const loc = locations[i];
const light = createLight();
light.position.set(loc.x, 20, loc.z);
light.target.position.set(loc.x, 0, loc.z);
loop.updatables.push(light);
scene.add(light);
scene.add(light.target);
// now figure out the timing and shine a light
const timeDelayMillis =
fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis);
new TWEEN.Tween(light)
.delay(timeDelayMillis)
.to({ intensity: 100 }, 10)
.chain(
new TWEEN.Tween(light.position)
.to({ y: 5 }, 5000)
.onComplete(() => {
scene.remove(light);
light.dispose();
})
)
.start();
world.lights = [...world.lights, light];
}
} else {
console.error(
"Got bad server response status & data of",
resp.status,
resp.data
);
vue.setAlert(
"Error With Server",
"There was an error retrieving your claims from the server."
);
}
} catch (error) {
console.error("Got exception contacting server:", error);
vue.setAlert(
"Error With Server",
"There was a problem retrieving your claims from the server."
);
}
}
/**
*
* @param giveClaim
* @returns {x:float, z:float} where -50 <= x & z < 50
*/
function locForGive(giveClaim, platformWidth, borderWidth) {
let loc;
if (giveClaim?.claim?.recipient?.identifier) {
// this is directly to a person
loc = locForEthrDid(giveClaim.claim.recipient.identifier);
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
} else if (giveClaim?.object?.isPartOf?.identifier) {
// this is probably to a project
const objId = giveClaim.object.isPartOf.identifier;
if (objId.startsWith(ENDORSER_ENTITY_PREFIX)) {
loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length));
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
}
}
if (!loc) {
// it must be outside our known addresses so let's put it somewhere random on the side
const leftSide = Math.random() < 0.5;
loc = {
x: leftSide
? -platformWidth / 2 - borderWidth / 2
: platformWidth / 2 + borderWidth / 2,
z: Math.random() * platformWidth - platformWidth / 2,
};
}
return loc;
}
/**
* Generate a deterministic x & z location based on the randomness of an ID.
*
* We'd like the location to fully map back to the original ID.
* This typically means we use half the ID for the x and half for the z.
*
* ... in this case: a ULID.
* We'll use the first half (13 characters) for the x coordinate and next 13 for the z.
* We recognize that this is only 3 characters = 15 bits = 32768 unique values
* for the random part for the first half. We also recognize that those random
* bits may be shared with previous ULIDs if they were generated in the same
* millisecond, and therefore much of the evenness of the distribution depends
* on the other dimension.
*
* Also: since the first 10 characters are time-based, we're going to reverse
* the order of the characters to make the randomness more evenly distributed.
* This is reversing the order of the 5-bit characters, not each of the bits.
* Also wik: the first characters of the second half might be the same as
* previous ULIDs if they were generated in the same millisecond. So it's
* best to have that last character be the most significant bit so that there
* is a more even distribution in that dimension.
*
* @param ulid
* @returns {x: float, z: float} where 0 <= x & z < 100
*/
function locForUlid(ulid) {
const xChars = ulid.substring(0, 13).split("").reverse().join("");
const zChars = ulid.substring(13, 26).split("").reverse().join("");
// from https://github.com/ulid/javascript/blob/5e9727b527aec5b841737c395a20085c4361e971/lib/index.ts#L21
const BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford's Base32
// We're currently only using 1024 possible x and z values
// because the display is pretty low-fidelity at this point.
const rawX = BASE32.indexOf(xChars[1]) * 32 + BASE32.indexOf(xChars[0]);
const rawZ = BASE32.indexOf(zChars[1]) * 32 + BASE32.indexOf(zChars[0]);
const x = (100 * rawX) / 1024;
const z = (100 * rawZ) / 1024;
return { x, z };
}
/**
* See locForUlid. Similar, but for ethr DIDs.
* @param did
* @returns {x: float, z: float} where 0 <= x & z < 100
*/
function locForEthrDid(did) {
// "did:ethr:0x..."
if (did.length < 51) {
return { x: 0, z: 0 };
} else {
const randomness = did.substring("did:ethr:0x".length);
// We'll use all the randomness for fully unique x & z values.
// But we'll only calculate this view with the first byte since our rendering resolution is low.
const xOff = parseInt(Number("0x" + randomness.substring(0, 2)), 10);
const x = (xOff * 100) / 256;
// ... and since we're reserving 20 bytes total for x, start z with character 20,
// again with one byte.
const zOff = parseInt(Number("0x" + randomness.substring(20, 22)), 10);
const z = (zOff * 100) / 256;
return { x, z };
}
}
function createLight() {
const light = new THREE.SpotLight(0xffffff, 0, 0, Math.PI / 8, 0.5, 0);
// eslint-disable-next-line @typescript-eslint/no-empty-function
light.tick = () => {};
return light;
}

View File

@@ -0,0 +1,29 @@
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
export function createTerrain(props) {
const loader = new TextureLoader();
const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg");
// w h
const geometry = new PlaneGeometry(props.width, props.height, 64, 64);
const material = new MeshLambertMaterial({
color: props.color,
flatShading: true,
map: height,
//displacementMap: height,
//displacementScale: 5,
});
const plane = new Mesh(geometry, material);
plane.position.set(0, 0, 0);
plane.rotation.x -= Math.PI * 0.5;
//Storing our original vertices position on a new attribute
plane.geometry.attributes.position.originalPosition =
plane.geometry.attributes.position.array;
// eslint-disable-next-line @typescript-eslint/no-empty-function
plane.tick = () => {};
return plane;
}

View File

@@ -0,0 +1,11 @@
import { Color, Scene } from "three";
function createScene(color) {
const scene = new Scene();
scene.background = new Color(color);
//scene.fog = new Fog(color, 60, 90);
return scene;
}
export { createScene };

View File

@@ -0,0 +1,33 @@
import { Clock } from "three";
const clock = new Clock();
class Loop {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
this.updatables = [];
}
start() {
this.renderer.setAnimationLoop(() => {
this.tick();
// render a frame
this.renderer.render(this.scene, this.camera);
});
}
stop() {
this.renderer.setAnimationLoop(null);
}
tick() {
const delta = clock.getDelta();
for (const object of this.updatables) {
object.tick(delta);
}
}
}
export { Loop };

View File

@@ -0,0 +1,33 @@
const setSize = (container, camera, renderer) => {
// These are great for full-screen, which adjusts to a window.
const height = window.innerHeight;
const width = window.innerWidth - 50;
// These are better for fitting in a container, which stays that size.
//const height = container.scrollHeight;
//const width = container.scrollWidth;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
};
class Resizer {
constructor(container, camera, renderer) {
// set initial size on load
setSize(container, camera, renderer);
window.addEventListener("resize", () => {
// set the size again if a resize occurs
setSize(container, camera, renderer);
// perform any custom actions
this.onResize();
});
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onResize() {}
}
export { Resizer };

View File

@@ -0,0 +1,38 @@
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { MathUtils } from "three";
function createControls(camera, canvas) {
const controls = new OrbitControls(camera, canvas);
//enable controls?
controls.enabled = true;
controls.autoRotate = false;
//controls.autoRotateSpeed = 0.2;
// control limits
// It's recommended to set some control boundaries,
// to prevent the user from clipping with the objects.
// y axis
controls.minPolarAngle = MathUtils.degToRad(40); // default
controls.maxPolarAngle = MathUtils.degToRad(75);
// x axis
// controls.minAzimuthAngle = ...
// controls.maxAzimuthAngle = ...
//smooth camera:
// remember to add to loop updatables to work
controls.enableDamping = true;
//controls.enableZoom = false;
controls.maxDistance = 250;
//controls.enablePan = false;
controls.tick = () => controls.update();
return controls;
}
export { createControls };

View File

@@ -0,0 +1,13 @@
import { WebGLRenderer } from "three";
function createRenderer() {
const renderer = new WebGLRenderer({ antialias: true });
// turn on the physically correct lighting model
// (The browser complains: "THREE.WebGLRenderer: the property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead." However, that changes the lighting in a way that doesn't look better.)
renderer.physicallyCorrectLights = true;
return renderer;
}
export { createRenderer };

View File

@@ -2,8 +2,11 @@
* Generic strings that could be used throughout the app.
*/
export enum AppString {
APP_NAME = "KickStart with Time",
VERSION = "0.1",
DEFAULT_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
//DEFAULT_ENDORSER_API_SERVER = "http://localhost:3000",
APP_NAME = "Kick-Start with Time",
PROD_ENDORSER_API_SERVER = "https://endorser.ch:3000",
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
}

View File

@@ -7,6 +7,7 @@ import {
Settings,
SettingsSchema,
} from "./tables/settings";
import { AppString } from "@/constants/app";
// a separate DB because the seed is super-sensitive data
type SensitiveTables = {
@@ -40,7 +41,12 @@ const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
*/
// create password and place password in localStorage
/**
* Create password and place password in localStorage.
*
* It's good practice to keep the data encrypted at rest, so we'll do that even
* if the secret is stored right next to the app.
*/
const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
@@ -57,5 +63,8 @@ db.version(1).stores(NonsensitiveSchemas);
// initialize, a la https://dexie.org/docs/Tutorial/Design#the-populate-event
db.on("populate", function () {
// ensure there's an initial entry for settings
db.settings.add({ id: MASTER_SETTINGS_KEY });
db.settings.add({
id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
});
});

View File

@@ -3,6 +3,8 @@ export type Account = {
dateCreated: string;
derivationPath: string;
did: string;
// stringified JSON containing underlying key material of type IIdentifier
// https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
identity: string;
publicKeyHex: string;
mnemonic: string;

View File

@@ -1,9 +1,12 @@
// a singleton
export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
lastViewedClaimId?: string;
showContactGivesInline?: boolean;
};

View File

@@ -1,3 +1,9 @@
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios";
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
export const SERVICE_ID = "endorser.ch";
@@ -9,6 +15,25 @@ export interface AgreeVerifiableCredential {
object: Record<any, any>;
}
export interface GiverInputInfo {
did?: string;
name?: string;
}
export interface ClaimResult {
success: { claimId: string; handleId: string };
error: { code: string; message: string };
}
export interface GenericClaim {
"@context": string;
"@type": string;
issuedAt: string;
// "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>;
}
export interface GiveServerRecord {
agentDid: string;
amount: number;
@@ -24,10 +49,11 @@ export interface GiveServerRecord {
export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": string;
agent: { identifier: string };
agent?: { identifier: string };
description?: string;
fulfills?: { "@type": string; identifier: string };
identifier?: string;
object: { amountOfThisGood: number; unitCode: string };
object?: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string };
}
@@ -38,3 +64,132 @@ export interface RegisterVerifiableCredential {
object: string;
recipient: { identifier: string };
}
export interface InternalError {
error: string; // for system logging
userMessage?: string; // for user display
}
// This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN";
export function isHiddenDid(did) {
return did === HIDDEN_DID;
}
/**
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
**/
export function didInfo(did, identifiers, contacts) {
const myId = R.find((i) => i.did === did, identifiers);
if (myId) {
return "You";
} else {
const contact = R.find((c) => c.did === did, contacts);
if (contact) {
return contact.name || "Someone Unnamed in Contacts";
} else if (!did) {
return "Unpecified Person";
} else if (isHiddenDid(did)) {
return "Someone Not In Network";
} else {
return "Someone Not In Contacts";
}
}
}
/**
* For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim
*
* @param identity
* @param fromDid may be null
* @param toDid
* @param description may be null; should have this or hours
* @param hours may be null; should have this or description
*/
export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
identity: IIdentifier,
fromDid: string,
toDid: string,
description: string,
hours: number,
fulfillsProjectHandleId?: string
): Promise<AxiosResponse<ClaimResult> | InternalError> {
// Make a claim
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
recipient: { identifier: toDid },
};
if (fromDid) {
vcClaim.agent = { identifier: fromDid };
}
if (description) {
vcClaim.description = description;
}
if (hours) {
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
}
if (fulfillsProjectHandleId) {
vcClaim.fulfills = {
"@type": "PlanAction",
identifier: fulfillsProjectHandleId,
};
}
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) {
return new Promise<InternalError>((resolve, reject) => {
reject({
error: "No private key",
message:
"Your identifier " +
identity.did +
" is not configured correctly. Use a different identifier.",
});
});
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return axios.post(url, payload, { headers });
}
// from https://stackoverflow.com/a/175787/845494
//
export function isNumeric(str: string): boolean {
return !isNaN(+str);
}
export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}

View File

@@ -1,100 +0,0 @@
/* import * as R from "ramda";
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { IIdentifier } from "@veramo/core";
import { Contact } from "../entity/contact";
import { Settings } from "../entity/settings";
import * as utility from "../utility/utility";
const MAX_LOG_LENGTH = 2000000;
export const DEFAULT_ENDORSER_API_SERVER = "https://endorser.ch:3000";
export const DEFAULT_ENDORSER_VIEW_SERVER = "https://endorser.ch";
export const LOCAL_ENDORSER_API_SERVER = "http://127.0.0.1:3000";
export const LOCAL_ENDORSER_VIEW_SERVER = "http://127.0.0.1:3001";
export const TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000";
export const TEST_ENDORSER_VIEW_SERVER = "https://test.endorser.ch:8080";
// for contents set in reducers
interface Payload<T> {
type: string;
payload: T;
}
interface LogMsg {
log: boolean;
msg: string;
}
export const appSlice = createSlice({
name: "app",
initialState: {
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
settings: null as Settings,
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
identifiers: null as Array<IIdentifier> | null,
// This is nullable because it is cached state from the DB...
// it'll be null if we haven't even loaded from the DB yet.
contacts: null as Array<Contact> | null,
viewServer: DEFAULT_ENDORSER_VIEW_SERVER,
logMessage: "",
advancedMode: false,
testMode: false,
},
reducers: {
addIdentifier: (state, contents: Payload<IIdentifier>) => {
state.identifiers = state.identifiers.concat([contents.payload]);
},
addLog: (state, contents: Payload<LogMsg>) => {
if (state.logMessage.length > MAX_LOG_LENGTH) {
state.logMessage =
"<truncated>\n..." +
state.logMessage.substring(
state.logMessage.length - MAX_LOG_LENGTH / 2
);
}
if (contents.payload.log) {
console.log(contents.payload.msg);
state.logMessage += "\n" + contents.payload.msg;
}
},
setAdvancedMode: (state, contents: Payload<boolean>) => {
state.advancedMode = contents.payload;
},
setContacts: (state, contents: Payload<Array<Contact>>) => {
state.contacts = contents.payload;
},
setContact: (state, contents: Payload<Contact>) => {
const index = R.findIndex(
(c) => c.did === contents.payload.did,
state.contacts
);
state.contacts[index] = contents.payload;
},
setHomeScreen: (state, contents: Payload<string>) => {
state.settings.homeScreen = contents.payload;
},
setIdentifiers: (state, contents: Payload<Array<IIdentifier>>) => {
state.identifiers = contents.payload;
},
setSettings: (state, contents: Payload<Settings>) => {
state.settings = contents.payload;
},
setTestMode: (state, contents: Payload<boolean>) => {
state.testMode = contents.payload;
},
setViewServer: (state, contents: Payload<string>) => {
state.viewServer = contents.payload;
},
},
});
export const appStore = configureStore({ reducer: appSlice.reducer });
*/

View File

@@ -10,18 +10,23 @@ import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faBurst,
faCalendar,
faChevronLeft,
faCircle,
faCircleCheck,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faCopy,
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFloppyDisk,
faFolderOpen,
faGift,
faHand,
faHouseChimney,
faLongArrowAltLeft,
@@ -35,6 +40,8 @@ import {
faRotate,
faShareNodes,
faSpinner,
faSquareCaretDown,
faSquareCaretUp,
faTrashCan,
faUser,
faUsers,
@@ -42,18 +49,23 @@ import {
} from "@fortawesome/free-solid-svg-icons";
library.add(
faBurst,
faCalendar,
faChevronLeft,
faCircle,
faCircleCheck,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faCopy,
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFloppyDisk,
faFolderOpen,
faGift,
faHand,
faHouseChimney,
faLongArrowAltLeft,
@@ -67,6 +79,8 @@ library.add(
faRotate,
faShareNodes,
faSpinner,
faSquareCaretDown,
faSquareCaretUp,
faTrashCan,
faUser,
faUsers,

View File

@@ -1,21 +1,29 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db";
/**
*
* @param to :RouteLocationNormalized
* @param from :RouteLocationNormalized
* @param next :NavigationGuardNext
*/
const enterOrStart = async (to, from, next) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
};
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: () =>
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"),
beforeEnter: async (to, from, next) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
},
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/about",
@@ -28,6 +36,7 @@ const routes: Array<RouteRecordRaw> = [
name: "account",
component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/confirm-contact",
@@ -45,11 +54,20 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "contact-amounts" */ "../views/ContactAmountsView.vue"
),
},
{
path: "/contact-qr",
name: "contact-qr",
component: () =>
import(
/* webpackChunkName: "contact-qr" */ "../views/ContactQRScanShowView.vue"
),
},
{
path: "/contacts",
name: "contacts",
component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/scan-contact",
@@ -103,6 +121,14 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
),
},
{
path: "/new-identifier",
name: "new-identifier",
component: () =>
import(
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
),
},
{
path: "/project",
name: "project",
@@ -114,6 +140,15 @@ const routes: Array<RouteRecordRaw> = [
name: "projects",
component: () =>
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/seed-backup",
name: "seed-backup",
component: () =>
import(
/* webpackChunkName: "seed-backup" */ "../views/SeedBackupView.vue"
),
},
{
path: "/start",
@@ -121,6 +156,14 @@ const routes: Array<RouteRecordRaw> = [
component: () =>
import(/* webpackChunkName: "start" */ "../views/StartView.vue"),
},
{
path: "/statistics",
name: "statistics",
component: () =>
import(
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
),
},
];
/** @type {*} */

View File

@@ -49,7 +49,8 @@ export async function testServerRegisterUser() {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const endorserApiServer =
settings?.apiServer || AppString.TEST_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/claim";
const headers = {
"Content-Type": "application/json",

View File

@@ -66,20 +66,22 @@
</span>
</div>
<!-- Friend referral requirement notice -->
<!-- Registration notice -->
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
<div
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
<p class="mb-4">
<b>Important:</b> before you can create a new project or commit time to
one, you need a friend to register you.
<b>Note:</b> Before you can publicly announce a new project or time
commitment, a friend needs to register you.
</p>
<button
id="btnShowQR"
<router-link
:to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
>
Share Your ID
</button>
Share Your Info
</router-link>
</div>
<!-- Identity Details -->
@@ -89,45 +91,65 @@
<div class="text-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ activeDid }}</code>
<button @click="copy(activeDid)" class="ml-2">
<button
@click="
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDidCopy">Copied!</span>
<span class="whitespace-nowrap ml-4">
<button
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md"
>
<fa icon="share-nodes" class="fa-fw"></fa>
</button>
<button
<router-link
:to="{ name: 'contact-qr' }"
class="text-xs uppercase bg-slate-500 text-white px-1.5 py-1 rounded-md ml-1"
>
<fa icon="qrcode" class="fa-fw"></fa>
</button>
</router-link>
</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ publicBase64 }}</code>
<button @click="copy(publicBase64)" class="ml-2">
<button
@click="
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showB64Copy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ publicHex }}</code>
<button @click="copy(publicHex)" class="ml-2">
<button
@click="
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showPubCopy">Copied!</span>
</div>
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
<code class="truncate">{{ derivationPath }}</code>
<button @click="copy(derivationPath)" class="ml-2">
<button
@click="
doCopyTwoSecRedo(derivationPath, () => (showDerCopy = !showDerCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDerCopy">Copied!</span>
</div>
</div>
@@ -140,12 +162,13 @@
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
<a
<router-link
:to="{ name: 'seed-backup' }"
href=""
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Backup Identifier Seed
</a>
</router-link>
<a
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="exportDatabase()"
@@ -160,7 +183,7 @@
<div class="text-slate-500 text-center">
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
</div>
<img src="img/sample-qr-code.png" class="w-full mb-3" />
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
<button
value="cancel"
@@ -208,6 +231,13 @@
<button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits
</button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
@@ -224,8 +254,42 @@
</div>
</div>
<div class="flex py-2">
Claim Server
<input
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="apiServerInput"
/>
<button
v-if="apiServerInput != apiServer"
class="px-4 rounded bg-red-500 border border-slate-400"
@click="onClickSaveApiServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)"
>
Use Prod
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)"
>
Use Test
</button>
<button
class="px-4 rounded bg-slate-200 border border-slate-400"
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)"
>
Use Local
</button>
</div>
<div v-if="numAccounts > 0" class="flex py-2">
Switch Account
Switch Identifier
<span v-for="accountNum in numAccounts" :key="accountNum">
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
#{{ accountNum }}
@@ -233,36 +297,35 @@
</span>
</div>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
<div>
<button class="text-blue-500">
<router-link
:to="{ name: 'statistics' }"
class="block text-center py-3"
>
See Achievements & Statistics
</router-link>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import "dexie-export-import";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
accessToken,
deriveAddress,
generateSeed,
newIdentifier,
} from "@/libs/crypto";
import { accessToken } from "@/libs/crypto";
import { AxiosError } from "axios/index";
import AlertMessage from "@/components/AlertMessage";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@@ -276,9 +339,13 @@ interface RateLimits {
nextWeekBeginDateTime: string;
}
@Component
@Component({ components: { AlertMessage } })
export default class AccountViewView extends Vue {
Constants = AppString;
activeDid = "";
apiServer = "";
apiServerInput = "";
derivationPath = "";
firstName = "";
lastName = "";
@@ -286,9 +353,22 @@ export default class AccountViewView extends Vue {
publicHex = "";
publicBase64 = "";
limits: RateLimits | null = null;
limitsMessage = "";
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
showContactGives = false;
copy = useClipboard().copy;
showDidCopy = false;
showDerCopy = false;
showB64Copy = false;
showPubCopy = false;
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text, fn) {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
}
handleChange() {
this.showContactGives = !this.showContactGives;
@@ -301,7 +381,7 @@ export default class AccountViewView extends Vue {
// 'created' hook runs when the Vue instance is first created
async created() {
// Uncomment to register this user on the test server.
// Uncomment this to register this user on the test server.
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
@@ -311,39 +391,20 @@ export default class AccountViewView extends Vue {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.lastName = settings?.lastName || "";
this.showContactGives = !!settings?.showContactGivesInline;
await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count();
if (this.numAccounts === 0) {
let address = ""; // 0x... ETH address, without "did:eth:"
let privateHex = "";
const mnemonic = generateSeed();
[address, privateHex, this.publicHex, this.derivationPath] =
deriveAddress(mnemonic);
const newId = newIdentifier(
address,
this.publicHex,
privateHex,
this.derivationPath
);
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: this.derivationPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
this.activeDid = newId.did;
}
const accounts = await accountsDB.accounts.toArray();
this.numAccounts = accounts.length;
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath;
@@ -351,12 +412,12 @@ export default class AccountViewView extends Vue {
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did,
});
this.checkLimits();
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.log("Telling user to clear cache because:", err);
"Clear your cache and start over (after data backup).";
console.error("Telling user to clear cache at page create because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
}
}
@@ -368,10 +429,12 @@ export default class AccountViewView extends Vue {
});
} catch (err) {
this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info.";
console.log("Telling user to clear cache because:", err);
this.alertTitle = "Error Creating Account";
this.isAlertVisible = true;
"Clear your cache and start over (after data backup).";
console.error(
"Telling user to clear cache after contact setting update because:",
err
);
this.alertTitle = "Error Updating Contact Setting";
}
}
@@ -389,22 +452,25 @@ export default class AccountViewView extends Vue {
this.alertTitle = "Download Started";
this.alertMessage = "See your downloads directory for the backup.";
this.isAlertVisible = true;
} catch (error) {
this.alertTitle = "Export Error";
this.alertMessage = "See console logs for more info.";
this.isAlertVisible = true;
console.error("Export Error:", error);
}
}
async checkLimits() {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/report/rateLimits";
this.loadingLimits = true;
this.limitsMessage = "";
const url = this.apiServer + "/api/report/rateLimits";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@@ -420,18 +486,14 @@ export default class AccountViewView extends Vue {
} catch (error: unknown) {
const serverError = error as AxiosError;
this.alertTitle = "Error from Server";
console.log("Bad response retrieving limits: ", serverError);
console.error("Bad response retrieving limits: ", serverError);
// Anybody know how to access items inside "response.data" without this?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = serverError.response?.data;
if (data.error.message) {
this.alertMessage = data.error.message;
} else {
this.alertMessage = "Bad server response. See logs for details.";
}
this.isAlertVisible = true;
this.limitsMessage = data?.error?.message || "Bad server response.";
}
this.loadingLimits = false;
}
async switchAccount(accountNum: number) {
@@ -457,31 +519,20 @@ export default class AccountViewView extends Vue {
};
}
async onClickSaveApiServer() {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
apiServer: this.apiServerInput,
});
this.apiServer = this.apiServerInput;
}
setApiServerInput(value) {
this.apiServerInput = value;
}
// This same popup code is in many files.
alertMessage = "";
alertTitle = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>

View File

@@ -109,28 +109,20 @@
</div>
</div>
</div>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { AppString } from "@/constants/app";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import {
AgreeVerifiableCredential,
@@ -140,24 +132,36 @@ import {
} from "@/libs/endorserServer";
import * as didJwt from "did-jwt";
import { AxiosError } from "axios";
import AlertMessage from "@/components/AlertMessage";
@Options({})
@Component({ components: { AlertMessage } })
export default class ContactsView extends Vue {
activeDid = "";
apiServer = "";
contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = [];
alertTitle = "";
alertMessage = "";
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const contactDid = this.$route.query.contactDid as string;
this.contact = (await db.contacts.get(contactDid)) || null;
try {
await db.open();
const contactDid = this.$route.query.contactDid as string;
this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact);
if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact);
}
} catch (err) {
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
}
}
@@ -166,16 +170,17 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
// load all the time I have given to them
try {
let result = [];
const url =
endorserApiServer +
this.apiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did) +
"&recipientDid=" +
@@ -189,7 +194,7 @@ export default class ContactsView extends Vue {
if (resp.status === 200) {
result = resp.data.data;
} else {
console.log(
console.error(
"Got bad response status & data of",
resp.status,
resp.data
@@ -197,11 +202,10 @@ export default class ContactsView extends Vue {
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
const url2 =
endorserApiServer +
this.apiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(contact.did) +
"&recipientDid=" +
@@ -215,7 +219,7 @@ export default class ContactsView extends Vue {
if (resp2.status === 200) {
result = R.concat(result, resp2.data.data);
} else {
console.log(
console.error(
"Got bad response status & data of",
resp2.status,
resp2.data
@@ -223,7 +227,6 @@ export default class ContactsView extends Vue {
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
const sortedResult: Array<GiveServerRecord> = R.sort(
@@ -235,7 +238,6 @@ export default class ContactsView extends Vue {
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
}
@@ -267,7 +269,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
if (identity.keys[0].privateKeyHex !== null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
@@ -282,8 +287,7 @@ export default class ContactsView extends Vue {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@@ -311,7 +315,6 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see.
this.alertTitle = "Error With Server";
this.alertMessage = userMessage;
this.isAlertVisible = true;
}
}
}
@@ -319,34 +322,6 @@ export default class ContactsView extends Vue {
cannotConfirmMessage() {
this.alertTitle = "Not Allowed";
this.alertMessage = "Only the recipient can confirm final receipt.";
this.isAlertVisible = true;
}
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>

View File

@@ -0,0 +1,145 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Contact Info
</h1>
<!--
Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
-->
<QRCodeVue3
:value="this.qrValue"
:cornersSquareOptions="{ type: 'extra-rounded' }"
:dotsOptions="{ type: 'square' }"
class="flex justify-center"
/>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as R from "ramda";
import { SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import AlertMessage from "@/components/AlertMessage";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@Component({
components: {
QRCodeVue3,
AlertMessage,
},
})
export default class ContactQRScanShow extends Vue {
activeDid = "";
apiServer = "";
qrValue = "";
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (!account) {
this.alertMessage = "You have no identity yet.";
} else {
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const contactInfo = {
iat: Date.now(),
iss: this.activeDid,
own: {
name: (settings?.firstName || "") + " " + (settings?.lastName || ""),
publicEncKey,
},
};
const alg = undefined;
const privateKeyHex: string = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex);
// create a JWT for the request
const vcJwt: string = await didJwt.createJWT(contactInfo, {
alg: alg,
issuer: identity.did,
signer: signer,
});
const viewPrefix = "https://endorser.ch/contact?jwt=";
this.qrValue = viewPrefix + vcJwt;
}
}
// This same popup code is in many files.
alertTitle = "";
alertMessage = "";
}
</script>

View File

@@ -49,6 +49,18 @@
Your Contacts
</h1>
<div class="flex justify-between py-2">
<span />
<span>
<router-link
:to="{ name: 'help' }"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
>
Help
</router-link>
</span>
</div>
<!-- New Contact -->
<div class="mb-4 flex">
<input
@@ -165,8 +177,11 @@
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span class="tooltiptext-left">
{{ givenByMeDescriptions[contact.did] || "Nothing" }}
<span
v-if="givenByMeDescriptions[contact.did]"
class="tooltiptext-left"
>
{{ givenByMeDescriptions[contact.did] }}
</span>
<button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@@ -187,8 +202,11 @@
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span class="tooltiptext-left">
{{ givenToMeDescriptions[contact.did] || "Nothing" }}
<span
v-if="givenToMeDescriptions[contact.did]"
class="tooltiptext-left"
>
{{ givenToMeDescriptions[contact.did] }}
</span>
<button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@@ -212,18 +230,11 @@
</div>
</li>
</ul>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
@@ -231,9 +242,8 @@ import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@@ -248,11 +258,12 @@ import {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@Options({
components: {},
@Component({
components: { AlertMessage },
})
export default class ContactsView extends Vue {
activeDid = "";
apiServer = "";
contacts: Array<Contact> = [];
contactInput = "";
// { "did:...": concatenated-descriptions } entry for each contact
@@ -278,6 +289,7 @@ export default class ContactsView extends Vue {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.showGiveNumbers = !!settings?.showContactGivesInline;
if (this.showGiveNumbers) {
@@ -294,7 +306,7 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
console.error(
@@ -303,12 +315,10 @@ export default class ContactsView extends Vue {
return;
}
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// load all the time I have given
try {
const url =
endorserApiServer +
this.apiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did);
const token = await accessToken(identity);
@@ -317,7 +327,7 @@ export default class ContactsView extends Vue {
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
console.log("All gifts you've given:", resp.data);
//console.log("All gifts you've given:", resp.data);
if (resp.status === 200) {
const contactDescriptions: Record<string, string> = {};
const contactConfirmed: Record<string, number> = {};
@@ -344,7 +354,7 @@ export default class ContactsView extends Vue {
this.givenByMeConfirmed = contactConfirmed;
this.givenByMeUnconfirmed = contactUnconfirmed;
} else {
console.log(
console.error(
"Got bad response status & data of",
resp.status,
resp.data
@@ -352,18 +362,16 @@ export default class ContactsView extends Vue {
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
// load all the time I have received
try {
const url =
endorserApiServer +
this.apiServer +
"/api/v2/report/gives?recipientDid=" +
encodeURIComponent(identity.did);
const token = await accessToken(identity);
@@ -372,7 +380,7 @@ export default class ContactsView extends Vue {
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
console.log("All gifts you've recieved:", resp.data);
//console.log("All gifts you've recieved:", resp.data);
if (resp.status === 200) {
const contactDescriptions: Record<string, string> = {};
const contactConfirmed: Record<string, number> = {};
@@ -398,7 +406,7 @@ export default class ContactsView extends Vue {
this.givenToMeConfirmed = contactConfirmed;
this.givenToMeUnconfirmed = contactUnconfirmed;
} else {
console.log(
console.error(
"Got bad response status & data of",
resp.status,
resp.data
@@ -406,12 +414,10 @@ export default class ContactsView extends Vue {
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your received time from the server.";
this.isAlertVisible = true;
}
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
}
@@ -469,7 +475,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
// Make a claim
const vcClaim: RegisterVerifiableCredential = {
@@ -477,7 +486,7 @@ export default class ContactsView extends Vue {
"@type": "RegisterAction",
agent: { identifier: identity.did },
object: SERVICE_ID,
recipient: { identifier: contact.did },
participant: { identifier: contact.did },
};
// Make a payload for the claim
const vcPayload = {
@@ -502,8 +511,7 @@ export default class ContactsView extends Vue {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@@ -513,13 +521,19 @@ export default class ContactsView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data);
if (resp.data?.success?.handleId) {
if (resp.data?.success?.embeddedRecordError) {
this.alertTitle = "Registration Still Unknown";
let message = "There was some problem with the registration.";
if (typeof resp.data.success.embeddedRecordError == "string") {
message += " " + resp.data.success.embeddedRecordError;
}
this.alertMessage = message;
} else if (resp.data?.success?.handleId) {
contact.registered = true;
db.contacts.update(contact.did, { registered: true });
this.alertTitle = "Registration Success";
this.alertMessage = contact.name + " has been registered.";
this.isAlertVisible = true;
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
@@ -536,22 +550,23 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see.
this.alertTitle = "Error With Server";
this.alertMessage = userMessage;
this.isAlertVisible = true;
}
}
}
}
async setVisibility(contact: Contact, visibility: boolean) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
this.apiServer +
"/api/report/" +
(visibility ? "canSeeMe" : "cannotSeeMe");
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
const headers = {
@@ -567,31 +582,31 @@ export default class ContactsView extends Vue {
db.contacts.update(contact.did, { seesMe: visibility });
} else {
this.alertTitle = "Error With Server";
console.log("Bad response setting visibility: ", resp.data);
console.error("Bad response setting visibility: ", resp.data);
if (resp.data.error?.message) {
this.alertMessage = resp.data.error?.message;
} else {
this.alertMessage = "Bad server response of " + resp.status;
}
this.isAlertVisible = true;
}
} catch (err) {
this.alertTitle = "Error With Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
async checkVisibility(contact: Contact) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
const headers = {
@@ -612,7 +627,6 @@ export default class ContactsView extends Vue {
" can " +
(visibility ? "" : "not ") +
"see your activity.";
this.isAlertVisible = true;
} else {
this.alertTitle = "Error With Server";
if (resp.data.error?.message) {
@@ -620,12 +634,10 @@ export default class ContactsView extends Vue {
} else {
this.alertMessage = "Bad server response of " + resp.status;
}
this.isAlertVisible = true;
}
} catch (err) {
this.alertTitle = "Error With Server";
this.alertMessage = err as string;
this.isAlertVisible = true;
}
}
@@ -648,7 +660,10 @@ export default class ContactsView extends Vue {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
// if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
@@ -670,15 +685,12 @@ export default class ContactsView extends Vue {
this.alertTitle = "Input Error";
this.alertMessage =
"This is not a valid number of hours: " + this.hourInput;
this.isAlertVisible = true;
} else if (!parseFloat(this.hourInput)) {
this.alertTitle = "Input Error";
this.alertMessage = "Giving 0 hours does nothing.";
this.isAlertVisible = true;
} else if (!identity) {
this.alertTitle = "Status Error";
this.alertMessage = "No identity is available.";
this.isAlertVisible = true;
} else {
// ask to confirm amount
let toFrom;
@@ -756,8 +768,7 @@ export default class ContactsView extends Vue {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@@ -770,7 +781,7 @@ export default class ContactsView extends Vue {
if (resp.data?.success?.handleId) {
this.alertTitle = "Done";
this.alertMessage = "Successfully logged time to the server.";
this.isAlertVisible = true;
if (fromDid === identity.did) {
const newList = R.clone(this.givenByMeUnconfirmed);
newList[toDid] = (newList[toDid] || 0) + amount;
@@ -796,7 +807,6 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see.
this.alertTitle = "Error With Server";
this.alertMessage = userMessage;
this.isAlertVisible = true;
}
}
}
@@ -814,32 +824,9 @@ export default class ContactsView extends Vue {
}
}
// This same popup code is in many files.
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
public showGiveAmountsClassNames() {
return {

View File

@@ -51,18 +51,20 @@
</h1>
<!-- Quick Search -->
<form id="QuickSearch" class="mb-4 flex">
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()">
<input
type="text"
v-model="searchTerms"
placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/>
<button
@click="search()"
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</button>
</form>
</div>
<!-- Result Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300">
@@ -151,14 +153,85 @@
</a>
</li>
</ul>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
@Options({
components: {},
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as R from "ramda";
import { accessToken } from "@/libs/crypto";
import AlertMessage from "@/components/AlertMessage";
@Component({
components: { AlertMessage },
})
export default class DiscoverView extends Vue {}
export default class DiscoverView extends Vue {
activeDid = "";
apiServer = "";
searchTerms = "";
// 'mounted' hook runs after initial render
async mounted() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
}
public async search() {
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, allAccounts);
//console.log("about to parse from", this.activeDid, account?.identity);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
} else {
// it's OK without auth... we just won't get any identifiers
}
const claimContents =
"claimContents=" + encodeURIComponent(this.searchTerms);
const claimType = "claimType=PlanAction";
const queryParams = [claimContents, claimType].join("&");
return fetch(this.apiServer + "/api/v2/report/claims?" + queryParams, {
method: "GET",
headers: headers,
})
.then(async (response) => {
if (response.status !== 200) {
const details = await response.text();
throw details;
}
return response.json();
})
.then((results) => {
if (results.data) {
console.log(results.data);
} else {
throw JSON.stringify(results);
}
})
.catch((e) => {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving projects.";
this.alertTitle = "Error";
});
}
alertMessage = "";
alertTitle = "";
}
</script>

View File

@@ -55,16 +55,53 @@
</h1>
<div>
<h2 class="text-xl font-semibold">Introduction</h2>
<p>
This app is a window into data that you and your friends own, focused on
gifts and collaboration.
</p>
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<p>
We are building networks of people who want to grow a gifting society.
First of all, you can record ways you've seen people give, and that
leaves a permanent record -- one that came from you, and the recipient
can prove it was for them. This is personally gratifying, but it extends
to broader work: volunteers can get confirmation of activity and
selectively show off their contributions and network.
</p>
<p>
You can also record projects and plans and invite others to collaborate.
Soon you'll be able to see when others are interested and see how much
they're willing to contribute, even if there are conditions.
</p>
<p>
This app uses the power of cryptography to build a reputation, recording
activity that you can share at your discretion. You put some activity
public, but your sensitive information is not shared with anyone,
including our services. This is in contrast to Meta and Google, who hold
your data and allow you use it. Those services are useful, but they have
the control; this app gives you the control.
</p>
<h2 class="text-xl font-semibold">How do I take my first action?</h2>
<p>
You need someone to register you -- usually the person who told you
about this app, on the Contacts
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
and after you have contacts, you can select any contact on the home page
and record your appreciation for... whatever. That is a claim recorded
on a custom ledger. The day after being registered, you'll be able to
register others; later, you can create projects, too.
</p>
<p>
Note that there are limits to how many each person can register, so you
may have to wait.
</p>
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
<p>
There are two parts to backup your data: the identifier secrets and the
other data such as settings, contacts, etc.
There are two sets of data to backup: the identifier secrets and the
other data that isn't quite a secret such as settings, contacts, etc.
</p>
<div class="px-4">
@@ -78,6 +115,10 @@
<li>
Click on "Backup Identifier Seed" and follow the instructions.
</li>
<li>
If you have other identifiers, switch to each one and repeat those
steps.
</li>
</ul>
<h2 class="text-xl font-semibold">
@@ -107,23 +148,10 @@
</h2>
<ul class="list-disc list-inside">
<li>
You only have one identifier at a time. If you have an identifier on
Your Identity <fa icon="circle-user" class="fa-fw" /> page, you'll
need to clear it out;
<a
href="https://www.lifewire.com/how-to-clear-cache-2617980"
class="text-blue-500"
>
here are some helpful instructions.
</a>
But beware! This will also clear out your settings and contact data,
so be sure to back that up first.
</li>
<li>
<router-link class="text-blue-500" to="/">
Go to the start
<router-link class="text-blue-500" to="/import-account">
Go to the import page
</router-link>
and choose "Yes" to enter the identity you backed up.
and enter the seed phrase you backed up.
</li>
</ul>
@@ -146,6 +174,63 @@
may also add their public key by adding another comma followed by the
key.
</p>
<h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info?
</h2>
<p>
If you don't see anything associated with a person, this is typically
because they have not given you permission to see their information. Ask
them to add you to their contact list and make sure the eye next to your
name is open like this
<fa icon="eye" class="fa-fw" /> and not closed like this
<fa icon="eye-slash" class="fa-fw" />.
</p>
<p>
Sometimes the reason you don't see something is because the search time
is limited. Go to the bottom and make sure to load all the data on a
list. If you still don't see it, try a search or view on a different
page.
</p>
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
<p>
Go
<router-link to="start" class="text-blue-500">
create another identity here.
</router-link>
</p>
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
<p>
See
<a href="https://endorser.ch/privacy-policy" class="text-blue-500">
the Endorser Service Privacy Policy.
</a>
</p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<p>
{{ package.version }}
</p>
<h2 class="text-xl font-semibold">
For any other questions, including remove your data:
</h2>
<p>
Contact us through
<a href="https://communitycred.org">CommunityCred.org</a>.
</p>
</div>
</section>
</template>
<script lang="ts">
import * as Package from "../../package.json";
import { Component, Vue } from "vue-facing-decorator";
@Component
export default class Help extends Vue {
package = Package;
}
</script>

View File

@@ -1,15 +1,341 @@
<template>
<section></section>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Time Safari
</h1>
<div class="mb-8">
<h1 class="text-2xl">Quick Action</h1>
<p>Choose a contact to whom to show appreciation:</p>
<!-- similar contact selection code is in multiple places -->
<div class="px-4">
<button
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
{{ contact.name }}
</button>
<span v-if="allContacts.length > 0">&nbsp;or&nbsp;</span>
<button @click="openDialog()" class="text-blue-500">
someone not specified
</button>
</div>
</div>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<div>
<h1 class="text-2xl">Latest Activity</h1>
<span :class="{ hidden: isHiddenSpinner }">
<fa icon="spinner" class="fa-spin-pulse"></fa>
Loading&hellip;
</span>
<ul>
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 py-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId"
>
You've seen all claims below:
</div>
<div class="flex">
<fa
icon="gift"
class="fa-fw flex-none pt-1 pr-2 text-slate-500"
></fa>
<!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
<span class="">{{ this.giveDescription(record) }}</span>
</div>
</li>
</ul>
</div>
</section>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</template>
<script lang="ts">
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
@Options({
components: {
HelloWorld,
},
components: { GiftedDialog, AlertMessage },
})
export default class HomeView extends Vue {}
export default class HomeView extends Vue {
activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = [];
apiServer = "";
feedAllLoaded = false;
feedData = [];
feedPreviousOldestId = null;
feedLastViewedId = null;
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
// 'created' hook runs when the Vue instance is first created
async created() {
try {
await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray();
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed();
} catch (err) {
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
}
}
updateAllFeed = async () => {
this.isHiddenSpinner = false;
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data);
//console.log("Feed data:", this.feedData);
this.feedAllLoaded = results.hitLimit;
this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId;
if (
this.feedLastViewedId == null ||
this.feedLastViewedId < results.data[0].jwtId
) {
// save it to storage
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId,
});
// but not for this page because we need to remember what it was before
}
}
})
.catch((e) => {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving feed data.";
this.alertTitle = "Error";
});
this.isHiddenSpinner = true;
};
retrieveClaims = async (endorserApiServer, identifier, beforeId) => {
//const afterQuery = afterId == null ? "" : "&afterId=" + afterId;
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
const account = R.find(
(acc) => acc.did === this.activeDid,
this.allAccounts
);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
} else {
// it's OK without auth... we just won't get any identifiers
}
return fetch(this.apiServer + "/api/v2/report/gives?" + beforeQuery, {
method: "GET",
headers: headers,
})
.then(async (response) => {
if (response.status !== 200) {
const details = await response.text();
throw details;
}
return response.json();
})
.then((results) => {
if (results.data) {
return results;
} else {
throw JSON.stringify(results);
}
});
};
giveDescription(giveRecord) {
let claim = giveRecord.fullClaim;
if (claim.claim) {
// it's probably a Verified Credential
claim = claim.claim;
}
// agent.did is for legacy data, before March 2023
const giver =
claim.agent?.identifier || claim.agent?.did || giveRecord.issuer;
const giverInfo = didInfo(giver, this.allAccounts, this.allContacts);
const gaveAmount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.description || "something unknown";
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientInfo = gaveRecipientId
? " to " + didInfo(gaveRecipientId, this.allAccounts, this.allContacts)
: "";
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
}
displayAmount(code, amt) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
}
currencyShortWordForCode(unitCode, single) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
openDialog(giver) {
this.$refs.customDialog.open(giver);
}
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
});
} else {
// action was "cancel" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
recordGive(giverDid, description, hours) {
if (this.activeDid == null) {
this.alertTitle = "Error";
this.alertMessage =
"You must select an identity before you can record a give.";
return;
}
if (!description && !hours) {
this.alertTitle = "Error";
this.alertMessage =
"You must enter a description or some number of hours.";
return;
}
const account = R.find(
(acc) => acc.did === this.activeDid,
this.allAccounts
);
//console.log("about to parse from", this.activeDid, account?.identity);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours
)
.then((result) => {
if (result.status != 201 || result.data?.error) {
console.log("Error with give result:", result);
this.alertTitle = "Error";
this.alertMessage =
result.data?.error?.message ||
"There was an error recording the give.";
} else {
this.alertTitle = "Success";
this.alertMessage = "That gift was recorded.";
//this.updateAllFeed(); // full update is overkill but we should show something
}
})
.catch((e) => {
// axios throws errors on 400 responses
console.log("Error with give caught:", e);
this.alertTitle = "Error";
this.alertMessage =
e.userMessage ||
e.response?.data?.error?.message ||
"There was an error recording the give.";
});
}
}
</script>

View File

@@ -93,8 +93,7 @@ export default class ImportAccountView extends Vue {
});
this.$router.push({ name: "account" });
} catch (err) {
console.log("Error!");
console.log(err);
console.error("Error saving mnemonic & updating settings:", err);
}
}
}

View File

@@ -49,8 +49,9 @@
<span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }"
><i class="fa-solid fa-spinner fa-spin-pulse"></i>
<span :class="{ hidden: isHiddenSpinner }">
<!-- icon no worky? -->
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving&hellip;</span
>
</button>
@@ -62,31 +63,25 @@
Cancel
</button>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core";
import AlertMessage from "@/components/AlertMessage";
interface VerifiableCredential {
"@context": string;
@@ -96,16 +91,15 @@ interface VerifiableCredential {
identifier?: string;
}
@Options({
components: {},
@Component({
components: { AlertMessage },
})
export default class NewEditProjectView extends Vue {
activeDid = "";
apiServer = "";
projectName = "";
description = "";
errorMessage = "";
alertTitle = "";
alertMessage = "";
projectId = localStorage.getItem("projectId") || "";
isHiddenSave = false;
@@ -116,25 +110,28 @@ export default class NewEditProjectView extends Vue {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.projectId) {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
console.error("Error: no account was found.");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.LoadProject(identity);
}
}
}
async LoadProject(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url =
endorserApiServer +
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const token = await accessToken(identity);
@@ -145,14 +142,13 @@ export default class NewEditProjectView extends Vue {
try {
const resp = await this.axios.get(url, { headers });
console.log(resp.status, resp.data);
if (resp.status === 200) {
const claim = resp.data.claim;
this.projectName = claim.name;
this.description = claim.description;
}
} catch (error) {
console.log(error);
console.error("Got error retrieving that project", error);
}
}
@@ -191,8 +187,7 @@ export default class NewEditProjectView extends Vue {
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/claim";
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@@ -201,7 +196,6 @@ export default class NewEditProjectView extends Vue {
try {
const resp = await this.axios.post(url, payload, { headers });
console.log("Got resp data:", resp.data);
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
@@ -218,7 +212,6 @@ export default class NewEditProjectView extends Vue {
const route = {
name: "project",
};
console.log(route);
that.$router.push(route);
},
2000,
@@ -226,20 +219,23 @@ export default class NewEditProjectView extends Vue {
);
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError;
if (serverError) {
this.isAlertVisible = true;
if (serverError.message) {
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError);
this.alertTitle = "User Message";
userMessage = serverError.message; // This is info for the user.
userMessage = serverError.response.data.error.message; // This is info for the user.
this.alertMessage = userMessage;
} else {
this.alertTitle = "Server Message";
this.alertMessage = JSON.stringify(serverError.toJSON());
}
} else {
console.log("Here's the full error trying to save the claim:", error);
console.error(
"Here's the full error trying to save the claim:",
error
);
this.alertTitle = "Claim Error";
this.alertMessage = error as string;
}
@@ -249,23 +245,20 @@ export default class NewEditProjectView extends Vue {
}
}
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public async onSaveProjectClick() {
this.isHiddenSave = true;
this.isHiddenSpinner = false;
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
console.error("Error: there is no account.");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.SaveProject(identity);
}
}
@@ -274,22 +267,8 @@ export default class NewEditProjectView extends Vue {
this.$router.back();
}
isAlertVisible = false;
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
// This same popup code is in many files.
alertTitle = "";
alertMessage = "";
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Identity
</h1>
<div class="flex justify-center py-12">
<span />
<span v-if="loading">
<span class="text-xl">Creating...&nbsp;</span>
<fa
icon="spinner"
class="fa-spin fa-spin-pulse"
color="green"
size="128"
></fa>
</span>
<span v-else>
<span class="text-xl">Created!</span>
<fa
icon="burst"
class="fa-beat px-12"
color="green"
style="
--fa-animation-duration: 1s;
--fa-animation-direction: reverse;
--fa-animation-iteration-count: 1;
--fa-beat-scale: 6;
"
></fa>
</span>
<span />
</div>
</section>
</template>
<script lang="ts">
import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component
export default class AccountViewView extends Vue {
loading = true;
async mounted() {
await accountsDB.open();
const mnemonic = generateSeed();
// address is 0x... ETH address, without "did:eth:"
const [address, privateHex, publicHex, derivationPath] =
deriveAddress(mnemonic);
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
const identity = JSON.stringify(newId);
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: derivationPath,
did: newId.did,
identity: identity,
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
this.loading = false;
setTimeout(() => {
this.$router.push({ name: "account" });
}, 1000);
}
}
</script>

View File

@@ -9,7 +9,7 @@
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
@@ -17,7 +17,7 @@
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
@@ -108,14 +108,50 @@
</button>
</div>
<button
@click="openDialog({ name: 'you', did: activeDid })"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
>
I gave...
</button>
<div>
<p>... or choose a contact who gave:</p>
<!-- similar contact selection code is in multiple places -->
<div class="px-4">
<button
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
class="text-blue-500"
>
&nbsp;{{ contact.name }},
</button>
<span v-if="allContacts.length > 0">&nbsp;or&nbsp;</span>
<button @click="openDialog()" class="text-blue-500">
someone not specified
</button>
</div>
</div>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<!-- Commit -->
<!--
<router-link
:to="{ name: 'new-edit-commitment' }"
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8"
>Make Commitment</router-link
>
-->
<!-- Commitments -->
<!--
<div class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">Commitments</h3>
@@ -142,6 +178,11 @@
</li>
</ul>
</div>
-->
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -149,25 +190,31 @@
import { AxiosError } from "axios";
import * as moment from "moment";
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { createAndSubmitGive } from "@/libs/endorserServer";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import AlertMessage from "@/components/AlertMessage";
@Options({
components: {},
@Component({
components: { GiftedDialog, AlertMessage },
})
export default class ProjectViewView extends Vue {
activeDid = "";
allContacts: Array<Contact> = [];
apiServer = "";
expanded = false;
name = "";
description = "";
truncatedDesc = "";
truncateLength = 40;
timeSince = "";
projectId = localStorage.getItem("projectId") || "";
projectId = localStorage.getItem("projectId") || ""; // handle ID
errorMessage = "";
onEditClick() {
@@ -175,7 +222,6 @@ export default class ProjectViewView extends Vue {
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
@@ -188,9 +234,10 @@ export default class ProjectViewView extends Vue {
}
async LoadProject(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// eslint-disable-next-line prettier/prettier
const url = endorserApiServer + "/api/claim/byHandle/" + encodeURIComponent(this.projectId);
const url =
this.apiServer +
"/api/claim/byHandle/" +
encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
@@ -199,7 +246,6 @@ export default class ProjectViewView extends Vue {
try {
const resp = await this.axios.get(url, { headers });
console.log("resp.status, resp.data", resp.status, resp.data);
if (resp.status === 200) {
const startTime = resp.data.startTime;
if (startTime != null) {
@@ -222,7 +268,7 @@ export default class ProjectViewView extends Vue {
this.errorMessage =
"Something went wrong retrieving that project." +
" See logs for more info.";
console.log("Error retrieving project:", error);
console.error("Error retrieving project:", error);
}
}
}
@@ -231,18 +277,100 @@ export default class ProjectViewView extends Vue {
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.allContacts = await db.contacts.toArray();
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
console.error("Problem! Should have a profile!");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.LoadProject(identity);
}
}
openDialog(contact) {
this.$refs.customDialog.open(contact);
}
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
});
} else {
// action was not "confirm" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
async recordGive(giverDid, description, hours) {
if (this.activeDid == null) {
this.alertTitle = "Error";
this.alertMessage =
"You must select an identity before you can record a give.";
return;
}
if (!description && !hours) {
this.alertTitle = "Error";
this.alertMessage =
"You must enter a description or some number of hours.";
return;
}
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
this.projectId
)
.then((result) => {
if (result.status != 201 || result.data?.error) {
console.log("Error with give result:", result);
this.alertTitle = "Error";
this.alertMessage =
result.data?.error?.message ||
"There was an error recording the give.";
} else {
this.alertTitle = "Success";
this.alertMessage = "That gift was recorded.";
//this.updateAllFeed(); // full update is overkill but we should show something
}
})
.catch((e) => {
// axios throws errors on 400 responses
console.log("Error with give caught:", e);
this.alertTitle = "Error";
this.alertMessage =
e.userMessage ||
e.response?.data?.error?.message ||
"There was an error recording the give.";
});
}
// This same popup code is in many files.
alertMessage = "";
alertTitle = "";
}
</script>

View File

@@ -70,116 +70,201 @@
<fa icon="plus" class="fa-fw"></fa>
</button>
<!-- Results List -->
<ul class="">
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
<!-- Loading Animation -->
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm truncate">
{{ project.description }}
<!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData">
<ul>
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
</div>
</a>
</li>
</ul>
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm truncate">
{{ project.description }}
</div>
</div>
</a>
</li>
</ul>
</InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll";
import AlertMessage from "@/components/AlertMessage";
@Options({
components: {},
/**
* Represents data about a project
**/
interface ProjectData {
/**
* Name of the project
**/
name: string;
/**
* Description of the project
**/
description: string;
/**
* URL referencing information about the project
**/
handleId: string;
/**
* The Identier of the project
**/
rowid: string;
}
@Component({
components: { InfiniteScroll, AlertMessage },
})
export default class ProjectsView extends Vue {
projects: { handleId: string; name: string; description: string }[] = [];
apiServer = "";
projects: ProjectData[] = [];
current: IIdentifier;
isLoading = false;
alertTitle = "";
alertMessage = "";
/**
* Core project data loader
* @param url the url used to fetch the data
* @param token Authorization token
**/
async dataLoader(url: string, token: string) {
const headers: { [key: string]: string } = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
try {
this.isLoading = true;
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const plans: ProjectData[] = resp.data.data;
for (const plan of plans) {
const { name, description, handleId = plan.fullIri, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
}
} else {
console.log(resp.status);
}
} catch (error) {
console.error("Got error loading projects:", error.message);
this.alertTitle = "Error";
this.alertMessage = "Got an error loading projects:" + error.message;
} finally {
this.isLoading = false;
}
}
/**
* Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load
**/
async loadMoreData(payload: boolean) {
if (this.projects.length > 0 && payload) {
const latestProject = this.projects[this.projects.length - 1];
const url = `${this.apiServer}/api/v2/report/plansByIssuer?beforeId=${latestProject.rowid}`;
const token = await accessToken(this.current);
await this.dataLoader(url, token);
}
}
/**
* Handle clicking on a project entry found in the list
* @param id of the project
**/
onClickLoadProject(id: string) {
console.log("projectId", id);
localStorage.setItem("projectId", id);
const route = {
name: "project",
};
console.log(route);
this.$router.push(route);
}
/**
* Load projects initially
* @param identity of the user
**/
async LoadProjects(identity: IIdentifier) {
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
const url = endorserApiServer + "/api/v2/report/plansByIssuer";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const plans = resp.data.data;
for (let i = 0; i < plans.length; i++) {
const plan = plans[i];
const data = {
name: plan.name,
description: plan.description,
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
handleId: plan.handleId || plan.fullIri,
};
this.projects.push(data);
}
}
} catch (error) {
console.log(error);
}
const url = `${this.apiServer}/api/v2/report/plansByIssuer`;
const token: string = await accessToken(identity);
await this.dataLoader(url, token);
}
// 'created' hook runs when the Vue instance is first created
/**
* 'created' hook runs when the Vue instance is first created
**/
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.log("Problem! Should have a profile!");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.LoadProjects(identity);
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
console.error("Problem! You need a profile!");
this.alertTitle = "Error!";
this.alertMessage = "Problem! You need a profile!";
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error("No identity found.");
}
this.current = identity;
this.LoadProjects(identity);
}
} catch (err) {
console.log(err);
this.alertTitle = "Error!";
this.alertMessage = "Problem! You need a profile!";
}
}
/**
* Handling clicking on the new project button
**/
onClickNewProject(): void {
localStorage.removeItem("projectId");
const route = {
name: "new-edit-project",
};
console.log(route);
this.$router.push(route);
}
}

View File

@@ -0,0 +1,130 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Seed Backup
</h1>
<div class="flex justify-between py-2">
<span />
<span>
<router-link
:to="{ name: 'help' }"
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
>
Help
</router-link>
</span>
</div>
<div v-if="activeAccount">
<p>
BEWARE: Anyone who gets hold of this mnemonic seed phrase will be able
impersonate you and take over any digital holdings based on it. So only
reveal it when you are in a private place out of sight of other eyes,
and only record it in something private -- don't take a screenshot or
send it to any online service.
</p>
<button
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
@click="showSeedPhrase"
>
Click here when you're ready to see it.
</button>
<p v-if="showSeed">{{ activeAccount.mnemonic }}</p>
</div>
<div v-else>You do not have an active identity.</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import * as R from "ramda";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import AlertMessage from "@/components/AlertMessage";
@Component({ components: { AlertMessage } })
export default class SeedBackupView extends Vue {
activeAccount = null;
showSeed = false;
alertMessage = "";
alertTitle = "";
// 'created' hook runs when the Vue instance is first created
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
} catch (err) {
console.error("Got an error loading an identity:", err);
this.alertTitle = "Error Loading Account";
this.alertMessage = "Got an error loading your seed data.";
}
}
showSeedPhrase() {
this.showSeed = true;
}
}
</script>

View File

@@ -35,7 +35,7 @@ import { Options, Vue } from "vue-class-component";
})
export default class StartView extends Vue {
public onClickYes() {
this.$router.push({ name: "account" });
this.$router.push({ name: "new-identifier" });
}
public onClickNo() {

View File

@@ -0,0 +1,215 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Achievements & Statistics
</h1>
<div>
Here is a view of the activity you can see.
<ul class="list-disc list-inside">
<li>Each identity and claim has a unique position.</li>
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
<li>Each will show at their time of appearance relative to all others.</li>
<li>Note that the ones on the left and right edges are randomized
because their data isn't all visible to you.
</li>
<!-- eslint-enable -->
</ul>
</div>
<div class="mt-3">
<div v-if="worldProperties.startTime">
<label>Time Range:&nbsp;</label>
{{ worldProperties.startTime }}
-
{{ worldProperties.endTime }}
</div>
<div v-if="worldProperties.animationDurationSeconds">
<label>Animation Time:&nbsp;</label>
{{ worldProperties.animationDurationSeconds }} seconds
</div>
</div>
<button class="float-right" @click="captureGraphics()">Screenshot</button>
<div id="scene-container" class="h-screen"></div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator";
import { World } from "@/components/World/World.js";
import AlertMessage from "@/components/AlertMessage";
interface WorldProperties {
startTime?: string;
endTime?: string;
}
@Component({ components: { AlertMessage, World } })
export default class StatisticsView extends Vue {
world: World;
worldProperties: WorldProperties = {};
// 'mounted' hook runs after initial render
mounted() {
try {
const container = document.querySelector("#scene-container");
console.log(container);
const newWorld = new World(container, this);
newWorld.start();
this.world = newWorld;
} catch (err) {
console.log(err);
}
}
public captureGraphics() {
/**
// from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#examples
// Adds a blank image
const dataBlob = document
.querySelector("#scene-container")
.firstChild.toBlob((blob) => {
const newImg = document.createElement("img");
const url = URL.createObjectURL(blob);
newImg.onload = () => {
// no longer need to read the blob so it's revoked
URL.revokeObjectURL(url);
};
newImg.src = url;
document.body.appendChild(newImg);
});
**/
/**
// Yields a blank page with the iframe below
const dataUrl = document
.querySelector("#scene-container")
.firstChild.toDataURL("image/png");
**/
/**
// Yields a blank page with the iframe below
const dataUrl = this.world.renderer.domElement.toDataURL("image/png");
**/
/**
// Show the image in a new tab
const iframe = `
<iframe
src="${dataUrl}"
frameborder="0"
style="border:0; top:0px; left:0px; bottom:0px; right:0px; width:100%; height:100%;"
allowfullscreen>
</iframe>`;
const win = window.open();
win.document.open();
win.document.write(iframe);
win.document.close();
**/
// from https://stackoverflow.com/a/17407392/845494
// This yields a file with funny formatting.
//const image = const dataUrl.replace("image/png", "image/octet-stream");
/**
// Yields a blank image at the bottom of the page
// from https://discourse.threejs.org/t/save-screenshot-on-server/39900/3
const img = new Image();
img.src = this.world.renderer.domElement.toDataURL();
document.body.appendChild(img);
**/
/**
* This yields an SVG that only shows white and black highlights
// from https://stackoverflow.com/questions/27632621/exporting-from-three-js-scene-to-svg-or-other-vector-format
**/
const rendererSVG = new SVGRenderer();
rendererSVG.setSize(window.innerWidth, window.innerHeight);
rendererSVG.render(this.world.scene, this.world.camera);
//document.body.appendChild(rendererSVG.domElement);
ExportToSVG(rendererSVG, "test.svg");
}
public setWorldProperty(propertyName, propertyValue) {
this.worldProperties[propertyName] = propertyValue;
}
alertTitle = "";
alertMessage = "";
}
function ExportToSVG(rendererSVG, filename) {
const XMLS = new XMLSerializer();
const svgfile = XMLS.serializeToString(rendererSVG.domElement);
const svgData = svgfile;
const preface = '<?xml version="1.0" standalone="no"?>\r\n';
const svgBlob = new Blob([preface, svgData], {
type: "image/svg+xml;charset=utf-8",
});
const svgUrl = URL.createObjectURL(svgBlob);
const downloadLink = document.createElement("a");
downloadLink.href = svgUrl;
downloadLink.download = filename;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
</script>

View File

@@ -7,4 +7,9 @@ module.exports = defineConfig({
topLevelAwait: true,
},
},
pwa: {
iconPaths: {
faviconSVG: "img/icons/safari-pinned-tab.svg",
},
},
});