Compare commits
2 Commits
0.2.2
...
why-migrat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa6cf0c9f6 | ||
| 99db5deb77 |
1
.gitignore
vendored
@@ -4,7 +4,6 @@ node_modules
|
|||||||
signature.bin
|
signature.bin
|
||||||
*.pem
|
*.pem
|
||||||
verified.txt
|
verified.txt
|
||||||
myenv
|
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
|
|||||||
76
CHANGELOG.md
@@ -9,80 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
## [0.2.2] - 2024.01.05
|
## [0.1.2] - 2023.11.01
|
||||||
### Added
|
|
||||||
- Check for notification capability on front screen
|
|
||||||
- Contact next-public-key-hash in manual textual input
|
|
||||||
- Confirmation for contact visibility change
|
|
||||||
- YAML rendering of full claim details
|
|
||||||
- Hints for onboarding on the contact screen
|
|
||||||
|
|
||||||
|
|
||||||
## [0.2.0] - 2024.01.04
|
|
||||||
### Added
|
|
||||||
- Contact next-public-key-hash
|
|
||||||
- Icon for Android
|
|
||||||
- More thorough messaging and testing for notifications
|
|
||||||
|
|
||||||
## [0.1.9] - 2024.01.01
|
|
||||||
### Added
|
|
||||||
- Import for contacts and settings
|
|
||||||
- Second download button for DuckDuckGo
|
|
||||||
### Changed
|
|
||||||
- Removed some keys from Dexie's IndexedDB declarations
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.8] - 2023.12.27- d26d1d360152a7d0e559b68486e85b72b88bd9ff
|
|
||||||
### Added
|
|
||||||
- DB logging for service-worker events
|
|
||||||
- Help page for notifications
|
|
||||||
- Test notification & web-push triggers inside app
|
|
||||||
- Check that the app is installed
|
|
||||||
### Fixed
|
|
||||||
- Project issuer display name
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
|
|
||||||
### Changed
|
|
||||||
- Icons
|
|
||||||
### Fixed
|
|
||||||
- Notification switch now shows message
|
|
||||||
- Prod/test server warning message at top of page
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
|
|
||||||
### Added
|
|
||||||
- Infinite scroll on home page
|
|
||||||
### Changed
|
|
||||||
- UI improvements
|
|
||||||
- Show web-push subscription info
|
|
||||||
- Icon
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
|
|
||||||
### Added
|
|
||||||
- Web push notifications (though not finalized)
|
|
||||||
- Credentials details page
|
|
||||||
- See more data without an ID
|
|
||||||
- Change units of a give
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db
|
|
||||||
### Added
|
|
||||||
- Offer on a project
|
|
||||||
### Changed
|
|
||||||
- Automatically set as visible when importing a contact
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
|
|
||||||
### Added
|
|
||||||
- Contact name editing
|
|
||||||
### Changed
|
|
||||||
- Don't show actions on front page if not registered.
|
|
||||||
### Removed
|
|
||||||
- Home page Notiwind test buttons
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.2] - 2023.11.01 - 7f6c93802911a030a89fe3706e18b5c17151e5bb
|
|
||||||
### Added
|
### Added
|
||||||
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
||||||
|
|||||||
212
README.md
@@ -1,4 +1,4 @@
|
|||||||
# TimeSafari.app - Crowd-Funder for Time - PWA
|
# kickstart-for-time-pwa
|
||||||
|
|
||||||
## Project setup
|
## Project setup
|
||||||
|
|
||||||
@@ -13,46 +13,49 @@ npm install
|
|||||||
npm run serve
|
npm run serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
|
||||||
|
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
### Lints and fixes files
|
### Lints and fixes files
|
||||||
```
|
```
|
||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
|
||||||
|
|
||||||
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
|
|
||||||
|
|
||||||
* Update the project.task.yaml & CHANGELOG.md & the version in package.json, run `npm install`, and commit.
|
|
||||||
|
|
||||||
* [Tag wth the new version.](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases)
|
|
||||||
|
|
||||||
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test". Also record what version is on production.
|
|
||||||
|
|
||||||
* `npm run build`
|
|
||||||
|
|
||||||
* `npx prettier --write ./sw_scripts/`
|
|
||||||
|
|
||||||
...to make sure the service worker scripts are in proper form. (It's only important if you changed something in that directory.)
|
|
||||||
|
|
||||||
* `cp sw_scripts/[ns]* dist/`
|
|
||||||
|
|
||||||
... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
|
|
||||||
|
|
||||||
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
|
|
||||||
|
|
||||||
* Revert src/constants/app.ts and package.json (if that was prod), edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
|
||||||
|
|
||||||
|
### Test key contents
|
||||||
|
|
||||||
|
See [this page](openssl_signing_console.rst)
|
||||||
|
|
||||||
### Register new user on test server
|
### Register new user on test server
|
||||||
|
|
||||||
|
New users require registration. This can be done with a claim payload like this
|
||||||
|
by an existing user:
|
||||||
|
|
||||||
|
```
|
||||||
|
const vcClaim = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "RegisterAction",
|
||||||
|
agent: { identifier: identity0.did },
|
||||||
|
object: SERVICE_ID,
|
||||||
|
participant: { identifier: newIdentity.did },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
On the test server, User #0 has rights to register others, so you can start
|
On the test server, User #0 has rights to register others, so you can start
|
||||||
playing one of two ways:
|
playing one of two ways:
|
||||||
|
|
||||||
- Import the keys for the test User `did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F` by importing this seed phrase:
|
- Import the keys for the test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` by importing this seed phrase:
|
||||||
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
`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).)
|
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
||||||
|
|
||||||
- Alternatively, register someone else under User #0 automatically:
|
- Alternatively, register someone else under User #0 automatically:
|
||||||
@@ -63,39 +66,14 @@ playing one of two ways:
|
|||||||
|
|
||||||
### Create multiple identifiers
|
### Create multiple identifiers
|
||||||
|
|
||||||
Under the "Your Identity" screen, click "Advanced", click "Switch Identity / No Identity", then "Add Another Identity...".
|
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
|
### Create keys with alternate tools
|
||||||
|
|
||||||
[This page](openssl_signing_console.rst) is a tool to create a JWT from a locally-generated keypair.
|
See [this page](openssl_signing_console.rst)
|
||||||
|
|
||||||
### Web-push
|
|
||||||
|
|
||||||
For your own web-push tests, change the push server URL in Advanced settings on the account page, and install Time Safari & push server on the same domain.
|
|
||||||
|
|
||||||
### Icons
|
|
||||||
|
|
||||||
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
|
|
||||||
|
|
||||||
### Manual walk-through
|
|
||||||
|
|
||||||
- Clear the browser cache for localhost for a new user.
|
|
||||||
- See that it's using the test API.
|
|
||||||
- On each page, verify the messaging.
|
|
||||||
- On the home page, see the feed without names, and see a message prompting to generate an ID.
|
|
||||||
- On the discovery page, check that they can see projects, and set a search area to see projects nearby.
|
|
||||||
- As User #0 in another browser on the test API, add a give & a project. (See User #0 details above.)
|
|
||||||
- With the new user on the home page, see the feed that shows User #0 in network but without the name.
|
|
||||||
- As the new user on the contacts page, add User #0 as a contact.
|
|
||||||
- On the home page, see the feed that shows User #0 with a name.
|
|
||||||
- Generate an ID.
|
|
||||||
- On the home page, check that it now prompts them to get registered.
|
|
||||||
- On the account page, check that they see messages on limits.
|
|
||||||
- Register the ID from User #0.
|
|
||||||
- As the new user on the home page, check that they can now record a gift.
|
|
||||||
- On the contacts page, check that they cannot register someone else yet.
|
|
||||||
- Walk through the functions on each page.
|
|
||||||
|
|
||||||
|
### Customize Vue configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
|
|
||||||
|
|
||||||
## Scenarios
|
## Scenarios
|
||||||
@@ -103,22 +81,17 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
|
|||||||
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
|
- 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:
|
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
|
||||||
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
`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).)
|
(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.
|
- 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.
|
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
|
||||||
|
|
||||||
### Clear/Reset data & restart
|
### Clear data & restart
|
||||||
|
|
||||||
* Clear cache for site. (In Chrome, go to `chrome://settings/cookies` and "all site data and permissions"; in Firefox, go to `about:preferences` and search for "cache" then "Manage Data", and also manually remove the IndexedDB data if the DBs still show.)
|
|
||||||
* Clear notification permission. (In Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".)
|
|
||||||
* Unregister service worker. (In Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.)
|
|
||||||
* Clear Cache Storage manually, possibly deleting the DB. (In Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)
|
|
||||||
|
|
||||||
(If you find more, add them to the HelpNotificationsView.vue file.)
|
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -129,10 +102,110 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
|
|||||||
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
|
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
|
||||||
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
|
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
|
||||||
|
|
||||||
* [Customize Vue configuration](https://cli.vuejs.org/config/).
|
```
|
||||||
|
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83
|
||||||
|
|
||||||
|
// Import an existing ID
|
||||||
|
export const importAndStoreIdentifier = async (mnemonic: string, mnemonicPassword: string, toLowercase: boolean, previousIdentifiers: Array<IIdentifier>) => {
|
||||||
|
|
||||||
### Kudos
|
// just to get rid of variability that might cause an error
|
||||||
|
mnemonic = mnemonic.trim().toLowerCase()
|
||||||
|
|
||||||
|
/**
|
||||||
|
// an approach I pieced together
|
||||||
|
// requires: yarn add elliptic
|
||||||
|
// ... plus:
|
||||||
|
// const EC = require('elliptic').ec
|
||||||
|
// const secp256k1 = new EC('secp256k1')
|
||||||
|
//
|
||||||
|
const keyHex: string = bip39.mnemonicToEntropy(mnemonic)
|
||||||
|
// returns a KeyPair from the elliptic.ec library
|
||||||
|
const keyPair = secp256k1.keyFromPrivate(keyHex, 'hex')
|
||||||
|
// this code is from did-provider-eth createIdentifier
|
||||||
|
const privateHex = keyPair.getPrivate('hex')
|
||||||
|
const publicHex = keyPair.getPublic('hex')
|
||||||
|
const address = didJwt.toEthereumAddress(publicHex)
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
|
||||||
|
// ... which almost works but the didJwt.toEthereumAddress is wrong
|
||||||
|
// requires: yarn add bip32
|
||||||
|
// ... plus: import * as bip32 from 'bip32'
|
||||||
|
//
|
||||||
|
const seed: Buffer = await bip39.mnemonicToSeed(mnemonic)
|
||||||
|
const root = bip32.fromSeed(seed)
|
||||||
|
const node = root.derivePath(UPORT_ROOT_DERIVATION_PATH)
|
||||||
|
const privateHex = node.privateKey.toString("hex")
|
||||||
|
const publicHex = node.publicKey.toString("hex")
|
||||||
|
const address = didJwt.toEthereumAddress('0x' + publicHex)
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
// from https://github.com/uport-project/veramo/discussions/346#discussioncomment-302234
|
||||||
|
// requires: yarn add @ethersproject/hdnode
|
||||||
|
// ... plus: import { HDNode } from '@ethersproject/hdnode'
|
||||||
|
**/
|
||||||
|
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic)
|
||||||
|
const rootNode: HDNode = hdnode.derivePath(UPORT_ROOT_DERIVATION_PATH)
|
||||||
|
const privateHex = rootNode.privateKey.substring(2) // original starts with '0x'
|
||||||
|
const publicHex = rootNode.publicKey.substring(2) // original starts with '0x'
|
||||||
|
let address = rootNode.address
|
||||||
|
|
||||||
|
const prevIds = previousIdentifiers || [];
|
||||||
|
|
||||||
|
if (toLowercase) {
|
||||||
|
const foundEqual = R.find(
|
||||||
|
(id) => utility.rawAddressOfDid(id.did) === address,
|
||||||
|
prevIds
|
||||||
|
)
|
||||||
|
if (foundEqual) {
|
||||||
|
// They're trying to create a lowercase version of one that exists in normal case.
|
||||||
|
// (We really should notify the user.)
|
||||||
|
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a normal-case version of the DID since a regular version exists."}))
|
||||||
|
} else {
|
||||||
|
address = address.toLowerCase()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// They're not trying to convert to lowercase.
|
||||||
|
const foundLower = R.find((id) =>
|
||||||
|
utility.rawAddressOfDid(id.did) === address.toLowerCase(),
|
||||||
|
prevIds
|
||||||
|
)
|
||||||
|
if (foundLower) {
|
||||||
|
// They're trying to create a normal case version of one that exists in lowercase.
|
||||||
|
// (We really should notify the user.)
|
||||||
|
appStore.dispatch(appSlice.actions.addLog({log: true, msg: "Will create a lowercase version of the DID since a lowercase version exists."}))
|
||||||
|
address = address.toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... derived keys and address..."}))
|
||||||
|
|
||||||
|
const newId = newIdentifier(address, publicHex, privateHex, UPORT_ROOT_DERIVATION_PATH)
|
||||||
|
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... created new ID..."}))
|
||||||
|
|
||||||
|
// awaiting because otherwise the UI may not see that a mnemonic was created
|
||||||
|
const savedId = await storeIdentifier(newId, mnemonic, mnemonicPassword)
|
||||||
|
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... stored new ID..."}))
|
||||||
|
return savedId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a totally new ID
|
||||||
|
export const createAndStoreIdentifier = async (mnemonicPassword) => {
|
||||||
|
|
||||||
|
// This doesn't give us the entropy/seed.
|
||||||
|
//const id = await agent.didManagerCreate()
|
||||||
|
|
||||||
|
const entropy = crypto.randomBytes(32)
|
||||||
|
const mnemonic = bip39.entropyToMnemonic(entropy)
|
||||||
|
appStore.dispatch(appSlice.actions.addLog({log: false, msg: "... generated mnemonic..."}))
|
||||||
|
|
||||||
|
return importAndStoreIdentifier(mnemonic, mnemonicPassword, false, [])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kudos
|
||||||
|
|
||||||
Gifts make the world go 'round!
|
Gifts make the world go 'round!
|
||||||
|
|
||||||
@@ -140,4 +213,3 @@ Gifts make the world go 'round!
|
|||||||
* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
|
* [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)
|
* [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)
|
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
|
||||||
* Time Safari logo assisted by [DALL-E in ChatGPT](https://chat.openai.com/g/g-2fkFE8rbu-dall-e)
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
JWT Creation & Verification
|
Prerequisites:
|
||||||
|
|
||||||
To run this in a script, see ./openssl_signing_console.sh
|
jq
|
||||||
|
|
||||||
Prerequisites: openssl, jq
|
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
||||||
|
|
||||||
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using
|
|
||||||
a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
|
||||||
|
|
||||||
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
||||||
|
|
||||||
@@ -18,22 +15,20 @@ openssl ec -in private.pem -pubout -out public.pem
|
|||||||
|
|
||||||
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
||||||
|
|
||||||
Next, create a payload object as a JSON object containing the claims you want to include in the JWT.
|
Next, create a payload object as a JSON object containing the claims you want to include in the JWT. For example schema.org :
|
||||||
For example schema.org :
|
|
||||||
|
|
||||||
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
|
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
|
||||||
|
|
||||||
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
||||||
|
|
||||||
Concatenate the encoded header, payload, and a secret to create the signing input:
|
Concatenate the encoded header, payload, and a secret to create the signing input:
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
Create the signature by signing the signing input with a ES256K algorithm and your secret.
|
Create the signature by signing the signing input with a ES256K algorithm and your secret. You can use the openssl command line utility to do this:
|
||||||
You can use the openssl command line utility to do this:
|
|
||||||
|
|
||||||
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
||||||
|
|
||||||
@@ -48,7 +43,7 @@ Authorization: Bearer $jwt
|
|||||||
|
|
||||||
To verify the JWT, you can use the openssl utility with the public key:
|
To verify the JWT, you can use the openssl utility with the public key:
|
||||||
|
|
||||||
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
|
openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
|
||||||
|
|
||||||
|
This will verify the signature and output Verified OK if the signature is valid. If the signature is not valid, it will output an error.
|
||||||
|
|
||||||
This will verify the signature and output "Verified OK" if the signature is valid.
|
|
||||||
If the signature is not valid, it will give an error response and output "Verification failure".
|
|
||||||
|
|||||||
@@ -1,39 +1,25 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Generate a JWT, with signature verified using OpenSSL
|
|
||||||
#
|
|
||||||
# Prerequisites: openssl, jq
|
|
||||||
#
|
|
||||||
# Usage: source ./openssl_signing_console.sh
|
|
||||||
#
|
|
||||||
# For a more complete explanation, see ./openssl_signing_console.rst
|
|
||||||
|
|
||||||
|
|
||||||
# Generate a key and extract the public part
|
|
||||||
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
||||||
openssl ec -in private.pem -pubout -out public.pem
|
openssl ec -in private.pem -pubout -out public.pem
|
||||||
|
|
||||||
# Use test data
|
|
||||||
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
||||||
|
|
||||||
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem | openssl base64 -e)
|
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
|
||||||
|
|
||||||
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature" | openssl base64 -d)
|
# Read binary signature from file and encode it to Base64 URL-Safe format
|
||||||
|
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Read binary signature and encode it to Base64 URL-Safe format
|
|
||||||
signature_b64=$(echo -n "$signature" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
|
||||||
|
|
||||||
# Construct the JWT
|
# Construct the JWT
|
||||||
jwt="$signing_input.$signature_b64"
|
jwt="$signing_input.$signature_b64"
|
||||||
|
|
||||||
echo Resulting JWT: $jwt
|
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
247
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari_Test",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.2.2",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "TimeSafari_Test",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.2.2",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@tweenjs/tween.js": "^21.0.0",
|
"@tweenjs/tween.js": "^21.0.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
|
||||||
"@veramo/core": "^5.4.1",
|
"@veramo/core": "^5.4.1",
|
||||||
"@veramo/credential-w3c": "^5.4.1",
|
"@veramo/credential-w3c": "^5.4.1",
|
||||||
"@veramo/data-store": "^5.4.1",
|
"@veramo/data-store": "^5.4.1",
|
||||||
@@ -34,10 +33,8 @@
|
|||||||
"ethereum-cryptography": "^2.1.2",
|
"ethereum-cryptography": "^2.1.2",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"ethr-did-resolver": "^8.1.2",
|
"ethr-did-resolver": "^8.1.2",
|
||||||
"git-describe": "^4.1.1",
|
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.2.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"localstorage-slim": "^2.5.0",
|
"localstorage-slim": "^2.5.0",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.4.3",
|
||||||
"merkletreejs": "^0.3.10",
|
"merkletreejs": "^0.3.10",
|
||||||
@@ -52,12 +49,9 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"three": "^0.156.1",
|
"three": "^0.156.1",
|
||||||
"ua-parser-js": "^1.0.37",
|
|
||||||
"util": "^0.12.5",
|
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.2",
|
"vue-facing-decorator": "^3.0.2",
|
||||||
"vue-qrcode-reader": "^5.4.1",
|
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.4",
|
||||||
"web-did-resolver": "^2.0.27"
|
"web-did-resolver": "^2.0.27"
|
||||||
},
|
},
|
||||||
@@ -65,7 +59,6 @@
|
|||||||
"@types/leaflet": "^1.9.4",
|
"@types/leaflet": "^1.9.4",
|
||||||
"@types/ramda": "^0.29.3",
|
"@types/ramda": "^0.29.3",
|
||||||
"@types/three": "^0.155.1",
|
"@types/three": "^0.155.1",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||||
"@typescript-eslint/parser": "^6.6.0",
|
"@typescript-eslint/parser": "^6.6.0",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
@@ -78,13 +71,13 @@
|
|||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.15",
|
||||||
"eslint": "^8.53.0",
|
"eslint": "^8.48.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"postcss": "^8.4.29",
|
"postcss": "^8.4.29",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.0.3",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "~5.2.2"
|
"typescript": "~5.2.2"
|
||||||
}
|
}
|
||||||
@@ -2827,9 +2820,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
|
||||||
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
|
"integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
@@ -2877,9 +2870,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "8.55.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz",
|
||||||
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
|
"integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
@@ -5504,12 +5497,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.13",
|
"version": "0.11.11",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
||||||
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
|
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@humanwhocodes/object-schema": "^2.0.1",
|
"@humanwhocodes/object-schema": "^1.2.1",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"minimatch": "^3.0.5"
|
"minimatch": "^3.0.5"
|
||||||
},
|
},
|
||||||
@@ -5531,9 +5524,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/object-schema": {
|
"node_modules/@humanwhocodes/object-schema": {
|
||||||
"version": "2.0.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
|
||||||
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
|
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@jest/create-cache-key-function": {
|
"node_modules/@jest/create-cache-key-function": {
|
||||||
@@ -8683,16 +8676,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/dom-webcodecs": {
|
|
||||||
"version": "0.1.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.10.tgz",
|
|
||||||
"integrity": "sha512-qQfLMw4yhtagKQApMQKaf21KZeJu3Psysbm/wLQ3mkpyBWY3x3dHCKFcYs43WEH+s8zgTSF0DvJUPWTtyZP0Dw=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/emscripten": {
|
|
||||||
"version": "1.39.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz",
|
|
||||||
"integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "8.44.4",
|
"version": "8.44.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.4.tgz",
|
||||||
@@ -8797,11 +8780,6 @@
|
|||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/js-yaml": {
|
|
||||||
"version": "4.0.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
|
||||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.13",
|
"version": "7.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
|
||||||
@@ -8830,11 +8808,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.10.4",
|
"version": "20.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
|
||||||
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
|
"integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/normalize-package-data": {
|
"node_modules/@types/normalize-package-data": {
|
||||||
@@ -8904,7 +8882,8 @@
|
|||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.3",
|
"version": "7.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz",
|
||||||
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw=="
|
"integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/send": {
|
"node_modules/@types/send": {
|
||||||
"version": "0.17.2",
|
"version": "0.17.2",
|
||||||
@@ -8984,12 +8963,6 @@
|
|||||||
"integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==",
|
"integrity": "sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/ua-parser-js": {
|
|
||||||
"version": "0.7.39",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz",
|
|
||||||
"integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/web-bluetooth": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "0.0.18",
|
"version": "0.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz",
|
||||||
@@ -9222,12 +9195,6 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ungap/structured-clone": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@unimodules/core": {
|
"node_modules/@unimodules/core": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@unimodules/core/-/core-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@unimodules/core/-/core-7.1.2.tgz",
|
||||||
@@ -11076,7 +11043,8 @@
|
|||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/array-buffer-byte-length": {
|
"node_modules/array-buffer-byte-length": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -11266,6 +11234,7 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
|
||||||
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
|
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -11546,15 +11515,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
},
|
},
|
||||||
"node_modules/barcode-detector": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-yA6gR5u5j22uw2eHSlFGzhYgnnQqx6hc4amDb/r0bKWl2gcDOqVE6SzUE6O87UzJ3ZhjJjM9uG/L9+D705HsKg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/dom-webcodecs": "^0.1.9",
|
|
||||||
"zxing-wasm": "1.0.0-rc.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/base-64": {
|
"node_modules/base-64": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
|
||||||
@@ -12083,6 +12043,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||||
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.1",
|
"function-bind": "^1.1.1",
|
||||||
"get-intrinsic": "^1.0.2"
|
"get-intrinsic": "^1.0.2"
|
||||||
@@ -14239,19 +14200,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "8.55.0",
|
"version": "8.51.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz",
|
||||||
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
|
"integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
"@eslint/eslintrc": "^2.1.4",
|
"@eslint/eslintrc": "^2.1.2",
|
||||||
"@eslint/js": "8.55.0",
|
"@eslint/js": "8.51.0",
|
||||||
"@humanwhocodes/config-array": "^0.11.13",
|
"@humanwhocodes/config-array": "^0.11.11",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@nodelib/fs.walk": "^1.2.8",
|
"@nodelib/fs.walk": "^1.2.8",
|
||||||
"@ungap/structured-clone": "^1.2.0",
|
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"cross-spawn": "^7.0.2",
|
"cross-spawn": "^7.0.2",
|
||||||
@@ -15911,6 +15871,7 @@
|
|||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
|
"integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-callable": "^1.1.3"
|
"is-callable": "^1.1.3"
|
||||||
}
|
}
|
||||||
@@ -16170,6 +16131,7 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"devOptional": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
@@ -16222,6 +16184,7 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||||
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||||
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.1",
|
"function-bind": "^1.1.1",
|
||||||
"has": "^1.0.3",
|
"has": "^1.0.3",
|
||||||
@@ -16286,30 +16249,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/git-describe": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/git-describe/-/git-describe-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-JC8ganO5kO80G8+XE98TDDjnMXQN3Estk3qdJuG2EGRF/l6zuMTMcN+8OSfQZ5FWpqIRLB015anWX4aSRgnxAQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/semver": "^7.3.8",
|
|
||||||
"lodash": "^4.17.21"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"semver": "^5.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/git-describe/node_modules/semver": {
|
|
||||||
"version": "5.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
|
||||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
@@ -16396,6 +16335,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-intrinsic": "^1.1.3"
|
"get-intrinsic": "^1.1.3"
|
||||||
},
|
},
|
||||||
@@ -16466,6 +16406,7 @@
|
|||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
|
||||||
"integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
|
"integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
|
||||||
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
@@ -16504,6 +16445,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||||
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -16515,6 +16457,7 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||||
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -16526,6 +16469,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
||||||
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.2"
|
"has-symbols": "^1.0.2"
|
||||||
},
|
},
|
||||||
@@ -17117,21 +17061,6 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-arguments": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
|
|
||||||
"dependencies": {
|
|
||||||
"call-bind": "^1.0.2",
|
|
||||||
"has-tostringtag": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
|
||||||
@@ -17203,6 +17132,7 @@
|
|||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -17307,20 +17237,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-generator-function": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
|
|
||||||
"dependencies": {
|
|
||||||
"has-tostringtag": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-glob": {
|
"node_modules/is-glob": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
@@ -17594,6 +17510,7 @@
|
|||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
|
||||||
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
|
"integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"which-typed-array": "^1.1.11"
|
"which-typed-array": "^1.1.11"
|
||||||
},
|
},
|
||||||
@@ -18624,6 +18541,7 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
},
|
},
|
||||||
@@ -19375,7 +19293,8 @@
|
|||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.clonedeep": {
|
"node_modules/lodash.clonedeep": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
@@ -23198,9 +23117,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.1.0",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
|
||||||
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
|
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
@@ -24718,11 +24637,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
|
||||||
"integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="
|
"integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="
|
||||||
},
|
},
|
||||||
"node_modules/sdp": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
|
|
||||||
},
|
|
||||||
"node_modules/secp256k1": {
|
"node_modules/secp256k1": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz",
|
||||||
@@ -26847,9 +26761,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ua-parser-js": {
|
"node_modules/ua-parser-js": {
|
||||||
"version": "1.0.37",
|
"version": "1.0.36",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz",
|
||||||
"integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
|
"integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -26864,6 +26778,8 @@
|
|||||||
"url": "https://github.com/sponsors/faisalman"
|
"url": "https://github.com/sponsors/faisalman"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
@@ -26929,9 +26845,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "5.25.3",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
|
||||||
},
|
},
|
||||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -27114,18 +27030,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz",
|
||||||
"integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ=="
|
"integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ=="
|
||||||
},
|
},
|
||||||
"node_modules/util": {
|
|
||||||
"version": "0.12.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
|
||||||
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
|
||||||
"dependencies": {
|
|
||||||
"inherits": "^2.0.3",
|
|
||||||
"is-arguments": "^1.0.4",
|
|
||||||
"is-generator-function": "^1.0.7",
|
|
||||||
"is-typed-array": "^1.1.3",
|
|
||||||
"which-typed-array": "^1.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -27360,18 +27264,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-qrcode-reader": {
|
|
||||||
"version": "5.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.4.1.tgz",
|
|
||||||
"integrity": "sha512-jwETIaRdumSCnXOpp0BkpZW8sySNFUfIPNOFa8oHAEmoSSdKK/ub5C1+3vMwokjU8iNERR2v/YhfBdcWDe0s5A==",
|
|
||||||
"dependencies": {
|
|
||||||
"barcode-detector": "2.1.1",
|
|
||||||
"webrtc-adapter": "8.2.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.2.5",
|
"version": "4.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
|
||||||
@@ -27947,18 +27839,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webrtc-adapter": {
|
|
||||||
"version": "8.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
|
|
||||||
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"sdp": "^3.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0",
|
|
||||||
"npm": ">=3.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/websocket-driver": {
|
"node_modules/websocket-driver": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||||
@@ -28037,6 +27917,7 @@
|
|||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
|
||||||
"integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
|
"integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"available-typed-arrays": "^1.0.5",
|
"available-typed-arrays": "^1.0.5",
|
||||||
"call-bind": "^1.0.2",
|
"call-bind": "^1.0.2",
|
||||||
@@ -28763,14 +28644,6 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/zxing-wasm": {
|
|
||||||
"version": "1.0.0-rc.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.0.0-rc.4.tgz",
|
|
||||||
"integrity": "sha512-SvVErHUZhzFqpqA2vpwmXeAPa6sgGdUCOkMCd5cMch6L1urZbZCZR8jb2+NI9bCfJRNkQi2ZjME9/NaiUFiSGg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/emscripten": "^1.39.9"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TimeSafari_Test",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.2.2",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@tweenjs/tween.js": "^21.0.0",
|
"@tweenjs/tween.js": "^21.0.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
|
||||||
"@veramo/core": "^5.4.1",
|
"@veramo/core": "^5.4.1",
|
||||||
"@veramo/credential-w3c": "^5.4.1",
|
"@veramo/credential-w3c": "^5.4.1",
|
||||||
"@veramo/data-store": "^5.4.1",
|
"@veramo/data-store": "^5.4.1",
|
||||||
@@ -34,10 +33,8 @@
|
|||||||
"ethereum-cryptography": "^2.1.2",
|
"ethereum-cryptography": "^2.1.2",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"ethr-did-resolver": "^8.1.2",
|
"ethr-did-resolver": "^8.1.2",
|
||||||
"git-describe": "^4.1.1",
|
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.2.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"localstorage-slim": "^2.5.0",
|
"localstorage-slim": "^2.5.0",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.4.3",
|
||||||
"merkletreejs": "^0.3.10",
|
"merkletreejs": "^0.3.10",
|
||||||
@@ -52,12 +49,9 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"three": "^0.156.1",
|
"three": "^0.156.1",
|
||||||
"ua-parser-js": "^1.0.37",
|
|
||||||
"util": "^0.12.5",
|
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.2",
|
"vue-facing-decorator": "^3.0.2",
|
||||||
"vue-qrcode-reader": "^5.4.1",
|
|
||||||
"vue-router": "^4.2.4",
|
"vue-router": "^4.2.4",
|
||||||
"web-did-resolver": "^2.0.27"
|
"web-did-resolver": "^2.0.27"
|
||||||
},
|
},
|
||||||
@@ -65,7 +59,6 @@
|
|||||||
"@types/leaflet": "^1.9.4",
|
"@types/leaflet": "^1.9.4",
|
||||||
"@types/ramda": "^0.29.3",
|
"@types/ramda": "^0.29.3",
|
||||||
"@types/three": "^0.155.1",
|
"@types/three": "^0.155.1",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||||
"@typescript-eslint/parser": "^6.6.0",
|
"@typescript-eslint/parser": "^6.6.0",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
@@ -78,13 +71,13 @@
|
|||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.15",
|
||||||
"eslint": "^8.53.0",
|
"eslint": "^8.48.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"postcss": "^8.4.29",
|
"postcss": "^8.4.29",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.0.3",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"typescript": "~5.2.2"
|
"typescript": "~5.2.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,54 @@
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
- show VC details... somehow:
|
- in endorser-push-server - mount folder for persistent sqlite DB outside of container
|
||||||
- 01 show my VCs - most interesting, or via search
|
- 40 notifications :
|
||||||
- 04 allow user to download chains of VCs, mine + ones I can see about me from others
|
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||||
- add VC confirmation
|
|
||||||
|
|
||||||
- copy button for seed
|
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
||||||
- .5 If notifications are not enabled, add message to front page with link/button to enable
|
|
||||||
- record donations vs gives
|
|
||||||
- make server endpoint for full English description of limits
|
|
||||||
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
|
|
||||||
- create a help-desk document & add screenshots
|
|
||||||
|
|
||||||
- 01 server - show all claim details when issued by the issuer
|
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
||||||
- 01 on Mac (& Windows?) desktop, add a help blurb so that they can find it again (since it doesn't show in Application list)
|
- .1 add instructions for map location selection
|
||||||
- bug - got error adding on Firefox user #0 as contact for themselves
|
|
||||||
- bug (that is hard to reproduce) - back-and-forth on discovery & project pages led to "You need an identity to load your projects." error on product page when I had an identity
|
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished
|
||||||
- bug (that is hard to reproduce) - in Chrome, install app then delete app and try from Chrome browser and see log errors "Uncaught TypeError: self.appendDailyLog is not a function"
|
|
||||||
- bug (that is hard to reproduce) - on the second 'give' recorded on prod it showed me as the agent
|
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
||||||
- 01 send visibility signal as a VC and store it
|
|
||||||
- 04 remove 'rowid' references (that are sqlite-specific); may involve server
|
- Home Feed & Quick Give screen :
|
||||||
- 04 look at other examples for better UI friend.tech
|
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
||||||
- 01 make the prod build copy the sw_scripts
|
- 01 quick action - send action, maybe choose via canvas tool
|
||||||
- .5 Add start date to project
|
- SEE: https://github.com/konvajs/vue-konva
|
||||||
- .3 check that Android shows "back" buttons on screens without bottom tray
|
|
||||||
- .1 Make give description text box into something that expands as they type?
|
- 24 Move to Vite assignee:matthew
|
||||||
|
|
||||||
|
- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView)
|
||||||
|
- .2 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
|
||||||
|
- .5 Add infinite scroll to gifts on the home page
|
||||||
|
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
||||||
|
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
|
||||||
|
- .1 when creating a plan, select location and then make sure you can deselect on Android
|
||||||
|
- .5 add link to further project / people when a project pays ahead
|
||||||
|
- .5 add project ID to the URL of the project-view, to make a project publicly-accessible
|
||||||
|
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
|
||||||
|
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist
|
||||||
|
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
||||||
|
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
||||||
|
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||||
|
- .1 Make give description text box into something that expands as they type
|
||||||
|
- .1 Make contact info specific to Time Safari - rather pointing at CommunityCred.org
|
||||||
|
|
||||||
|
- Discuss whether the remaining tasks are worthwhile before MVP release.
|
||||||
|
|
||||||
|
- 04 allow user to download claims, mine + ones I can see about me from others
|
||||||
|
- 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path)
|
||||||
|
- .5 on ProjectView page, show immediate feedback when a gift is given (on list?) -- and consider the same for Home & Contacts pages
|
||||||
- .5 customize favicon assignee-group:ui
|
- .5 customize favicon assignee-group:ui
|
||||||
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
||||||
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
||||||
- .5 Display a more appealing confirmation on the map when erasing the marker
|
- .5 Display a more appealing confirmation on the map when erasing the marker
|
||||||
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
|
- .5 make a VC details page
|
||||||
- switch some checks for activeDid to check for isRegistered
|
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
||||||
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
- .1 remove firstName (& lastName) from localStorage
|
||||||
- .5 fix cert generation on server (since it didn't happen automatically for Nov 30)
|
|
||||||
- warn if they're using the web (android only?)
|
|
||||||
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
|
|
||||||
https://web.dev/articles/get-installed-related-apps
|
|
||||||
|
|
||||||
- 04 fix lack of initial notification in Firefox (on MacOS, maybe others)
|
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
@@ -47,23 +57,26 @@ tasks:
|
|||||||
|
|
||||||
- stats v1 :
|
- stats v1 :
|
||||||
- 01 show numeric stats
|
- 01 show numeric stats
|
||||||
- 04 show different graphic for projects vs people (gnome?) on world
|
- 04 show different graphic for projects vs people on world
|
||||||
- 01 link to world for specific stats
|
- 01 link to world for specific stats
|
||||||
- .5 don't load another instance of a bush if it already exists
|
- .5 don't load another instance of a bush if it already exists
|
||||||
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
|
||||||
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
|
||||||
|
|
||||||
- .5 show seed phrase in a QR code for transfer to another device
|
- Release Minimum Viable Product :
|
||||||
- .5 on DiscoverView, switch to a filter UI (eg. just from friend
|
- 08 thorough testing for errors & edge cases
|
||||||
- .5 don't show "Offer" on project screen if they aren't registered
|
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
||||||
|
- Add disclaimers.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- .5 show seed phrase in a QR code for transfer to another device
|
||||||
|
|
||||||
- 24 Move to Vite
|
|
||||||
- 32 accept images for projects
|
- 32 accept images for projects
|
||||||
- 32 accept images for contacts
|
- 32 accept images for contacts
|
||||||
- import project interactions from GitHub/GitLab and manage signing
|
|
||||||
|
|
||||||
- show total time offered to & fulfilled to a project
|
|
||||||
- show total time offered by & fulfilled by a contact
|
|
||||||
|
|
||||||
- linking between projects or plans :
|
- linking between projects or plans :
|
||||||
- show total time given to & from a project
|
- show total time given to & from a project
|
||||||
@@ -71,10 +84,6 @@ tasks:
|
|||||||
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
||||||
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
|
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
|
||||||
|
|
||||||
- .5 add "back" button to all screens that aren't part of the bottom tray
|
|
||||||
- .5 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
|
|
||||||
- .5 Replace Gifted/Give in ContactsView with GiftedDialog
|
|
||||||
|
|
||||||
- Stats :
|
- Stats :
|
||||||
- 01 point out user's location on the world
|
- 01 point out user's location on the world
|
||||||
- 01 present a credential selected from the stats
|
- 01 present a credential selected from the stats
|
||||||
@@ -86,25 +95,23 @@ tasks:
|
|||||||
- automated tests, eg. cypress
|
- automated tests, eg. cypress
|
||||||
|
|
||||||
- Notifications (wake on the phone, push notifications)
|
- Notifications (wake on the phone, push notifications)
|
||||||
- 02 change push server so that the web-push/subscribe call sets up a thread for the 10-seconds-later push notification, but returns immediately to the callee
|
|
||||||
- pull instead of push, maybe via scheduled runs
|
|
||||||
- have a notification pop-up on Mac screen
|
|
||||||
|
|
||||||
- Connect with phone contacts
|
- Connect with phone contacts
|
||||||
|
|
||||||
- Multiple identities
|
- Multiple identities
|
||||||
|
|
||||||
- Support KERI AIDs
|
- Peer DID
|
||||||
- Support Peer DIDs
|
|
||||||
- Support messaging through DIDComm
|
- DIDComm
|
||||||
- Write to or read from a different ledger (eg. private ACDC, EAS & attest.sh)
|
|
||||||
|
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
|
||||||
|
|
||||||
- Do we want split first name & last name?
|
- Do we want split first name & last name?
|
||||||
|
|
||||||
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
- 40 notifications v+ :
|
||||||
- 16 From the home screen, make the quick action even easier.
|
- pull, w/ scheduled runs
|
||||||
|
|
||||||
- allow some gives even if they aren't registered - maybe someday as a gift to the world, but we really want this to be built via personal connections
|
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
|
||||||
|
|
||||||
log:
|
log:
|
||||||
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 463 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 799 B |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 215 B |
177
sample.txt
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
|
||||||
|
> kickstart-for-time-pwa@0.1.0 build
|
||||||
|
> vue-cli-service build
|
||||||
|
|
||||||
|
All browser targets in the browserslist configuration have supported ES module.
|
||||||
|
Therefore we don't build two separate bundles for differential loading.
|
||||||
|
|
||||||
|
|
||||||
|
WARNING Compiled with 5 warnings6:06:43 PM
|
||||||
|
|
||||||
|
[eslint]
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/components/World/components/objects/landmarks.js
|
||||||
|
98:11 warning Unexpected console statement no-console
|
||||||
|
133:7 warning Unexpected console statement no-console
|
||||||
|
144:5 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/router/index.ts
|
||||||
|
210:3 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/AccountViewView.vue
|
||||||
|
362:7 warning Unexpected console statement no-console
|
||||||
|
375:7 warning Unexpected console statement no-console
|
||||||
|
404:7 warning Unexpected console statement no-console
|
||||||
|
516:7 warning Unexpected console statement no-console
|
||||||
|
536:7 warning Unexpected console statement no-console
|
||||||
|
630:5 warning Unexpected console statement no-console
|
||||||
|
682:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactAmountsView.vue
|
||||||
|
206:9 warning Unexpected console statement no-console
|
||||||
|
233:9 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactGiftingView.vue
|
||||||
|
244:9 warning Unexpected console statement no-console
|
||||||
|
267:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactsView.vue
|
||||||
|
340:9 warning Unexpected console statement no-console
|
||||||
|
577:9 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/DiscoverView.vue
|
||||||
|
315:9 warning Unexpected console statement no-console
|
||||||
|
343:7 warning Unexpected console statement no-console
|
||||||
|
390:9 warning Unexpected console statement no-console
|
||||||
|
423:7 warning Unexpected console statement no-console
|
||||||
|
532:9 warning Unexpected console statement no-console
|
||||||
|
575:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/HomeView.vue
|
||||||
|
349:9 warning Unexpected console statement no-console
|
||||||
|
498:9 warning Unexpected console statement no-console
|
||||||
|
521:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/IdentitySwitcherView.vue
|
||||||
|
142:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportAccountView.vue
|
||||||
|
123:9 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportDerivedAccountView.vue
|
||||||
|
159:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/NewEditProjectView.vue
|
||||||
|
183:9 warning Unexpected console statement no-console
|
||||||
|
215:7 warning Unexpected console statement no-console
|
||||||
|
297:13 warning Unexpected console statement no-console
|
||||||
|
320:11 warning Unexpected console statement no-console
|
||||||
|
345:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectViewView.vue
|
||||||
|
387:9 warning Unexpected console statement no-console
|
||||||
|
421:7 warning Unexpected console statement no-console
|
||||||
|
457:7 warning Unexpected console statement no-console
|
||||||
|
552:9 warning Unexpected console statement no-console
|
||||||
|
554:11 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectsView.vue
|
||||||
|
131:9 warning Unexpected console statement no-console
|
||||||
|
144:7 warning Unexpected console statement no-console
|
||||||
|
221:9 warning Unexpected console statement no-console
|
||||||
|
237:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
/home/matthew/projects/kick-starter-for-time-pwa/src/views/SeedBackupView.vue
|
||||||
|
94:7 warning Unexpected console statement no-console
|
||||||
|
|
||||||
|
✖ 44 problems (0 errors, 44 warnings)
|
||||||
|
|
||||||
|
|
||||||
|
You may use special comments to disable some warnings.
|
||||||
|
Use // eslint-disable-next-line to ignore the next line.
|
||||||
|
Use /* eslint-disable */ to ignore all warnings in a file.
|
||||||
|
warning
|
||||||
|
|
||||||
|
/models/lupine_plant/textures/lambert2SG_baseColor.png is 3.75 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit.
|
||||||
|
|
||||||
|
warning
|
||||||
|
|
||||||
|
/models/lupine_plant/textures/lambert2SG_normal.png is 4.91 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit.
|
||||||
|
|
||||||
|
warning
|
||||||
|
|
||||||
|
asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
|
||||||
|
This can impact web performance.
|
||||||
|
Assets:
|
||||||
|
js/project.44f30c9f.js (318 KiB)
|
||||||
|
js/statistics.8a97010a.js (586 KiB)
|
||||||
|
js/chunk-vendors.a4845bfb.js (411 KiB)
|
||||||
|
js/705.f6a6ce2a.js (252 KiB)
|
||||||
|
img/textures/leafy-autumn-forest-floor.jpg (705 KiB)
|
||||||
|
models/lupine_plant/textures/lambert2SG_baseColor.png (3.58 MiB)
|
||||||
|
models/lupine_plant/textures/lambert2SG_normal.png (4.69 MiB)
|
||||||
|
|
||||||
|
warning
|
||||||
|
|
||||||
|
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
|
||||||
|
Entrypoints:
|
||||||
|
app (447 KiB)
|
||||||
|
js/chunk-vendors.a4845bfb.js
|
||||||
|
css/app.8f21529c.css
|
||||||
|
js/app.8833cebc.js
|
||||||
|
|
||||||
|
|
||||||
|
File Size Gzipped
|
||||||
|
|
||||||
|
dist/js/statistics.8a97010a.js 585.72 KiB 148.80 KiB
|
||||||
|
dist/js/chunk-vendors.a4845bfb.js 411.44 KiB 137.82 KiB
|
||||||
|
dist/js/project.44f30c9f.js 317.61 KiB 78.67 KiB
|
||||||
|
dist/js/705.f6a6ce2a.js 251.66 KiB 87.12 KiB
|
||||||
|
dist/js/891.33615e4f.js 147.32 KiB 42.09 KiB
|
||||||
|
dist/js/153.e2c8e249.js 146.26 KiB 42.21 KiB
|
||||||
|
dist/js/820.13565d16.js 66.10 KiB 18.33 KiB
|
||||||
|
dist/js/contact-qr.e170ec33.js 54.85 KiB 15.63 KiB
|
||||||
|
dist/js/772.7b4c53a7.js 30.29 KiB 7.21 KiB
|
||||||
|
dist/js/361.898a4525.js 27.40 KiB 8.19 KiB
|
||||||
|
dist/js/account.77d86130.js 17.51 KiB 5.93 KiB
|
||||||
|
dist/js/app.8833cebc.js 17.31 KiB 5.84 KiB
|
||||||
|
dist/js/contacts.3fc90ff8.js 16.94 KiB 5.52 KiB
|
||||||
|
dist/js/discover.24106939.js 15.30 KiB 5.22 KiB
|
||||||
|
dist/js/536.3bb13201.js 15.23 KiB 4.84 KiB
|
||||||
|
dist/workbox-5b385ed2.js 14.11 KiB 4.93 KiB
|
||||||
|
dist/js/home.218b99dd.js 13.89 KiB 4.97 KiB
|
||||||
|
dist/js/help.50d3117b.js 12.49 KiB 4.38 KiB
|
||||||
|
dist/js/projects.417a6cb7.js 8.71 KiB 3.00 KiB
|
||||||
|
dist/js/contact-amounts.a32b0ccd.js 8.44 KiB 3.25 KiB
|
||||||
|
dist/js/229.120e09bf.js 7.99 KiB 2.72 KiB
|
||||||
|
dist/js/identity-switcher.c7937333.js 7.44 KiB 2.52 KiB
|
||||||
|
dist/js/new-edit-project.0552181b.js 7.36 KiB 3.11 KiB
|
||||||
|
dist/js/300.dcaeb2a3.js 6.56 KiB 3.24 KiB
|
||||||
|
dist/js/seed-backup.76a0f7b3.js 3.99 KiB 1.97 KiB
|
||||||
|
dist/js/import-derive.c688d4b8.js 3.81 KiB 1.82 KiB
|
||||||
|
dist/js/import-account.c3fa35fd.js 3.54 KiB 1.66 KiB
|
||||||
|
dist/js/new-edit-account.bb763be2.js 3.39 KiB 1.51 KiB
|
||||||
|
dist/js/431.5a6d64e0.js 3.38 KiB 2.56 KiB
|
||||||
|
dist/service-worker.js 3.37 KiB 1.38 KiB
|
||||||
|
dist/js/scan-contact.46be989a.js 2.79 KiB 1.18 KiB
|
||||||
|
dist/js/start.091a7740.js 2.70 KiB 1.30 KiB
|
||||||
|
dist/js/new-identifier.bb379420.js 2.12 KiB 1.18 KiB
|
||||||
|
dist/js/93.b873dbbf.js 2.08 KiB 1.61 KiB
|
||||||
|
dist/js/new-edit-commitment.9248d367.j 1.96 KiB 1.05 KiB
|
||||||
|
s
|
||||||
|
dist/js/confirm-contact.02004d1d.js 1.89 KiB 1.04 KiB
|
||||||
|
dist/js/858.ae4c08ec.js 0.97 KiB 0.78 KiB
|
||||||
|
dist/css/app.8f21529c.css 18.41 KiB 4.39 KiB
|
||||||
|
dist/css/discover.73ee9bd3.css 14.77 KiB 6.25 KiB
|
||||||
|
dist/css/new-edit-project.73ee9bd3.css 14.77 KiB 6.25 KiB
|
||||||
|
dist/css/contacts.abb5e493.css 0.40 KiB 0.23 KiB
|
||||||
|
dist/css/contact-amounts.5b26ccd4.css 0.31 KiB 0.20 KiB
|
||||||
|
dist/css/home.828bc66e.css 0.25 KiB 0.19 KiB
|
||||||
|
dist/css/project.828bc66e.css 0.25 KiB 0.19 KiB
|
||||||
|
dist/css/statistics.828bc66e.css 0.25 KiB 0.19 KiB
|
||||||
|
|
||||||
|
Images and other types of assets omitted.
|
||||||
|
Build at: 2023-09-07T10:06:43.972Z - Hash: 2b39fcd4d0e78263 - Time: 32016ms
|
||||||
|
|
||||||
|
DONE Build complete. The dist directory is ready to be deployed.
|
||||||
|
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
|
||||||
|
|
||||||
394
src/App.vue
@@ -156,32 +156,28 @@
|
|||||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
>
|
>
|
||||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
<p v-if="serviceWorkerReady" class="text-lg mb-4">
|
<p class="text-lg mb-4">
|
||||||
Would you like to <b>turn on</b> notifications for this app?
|
Would you like to <b>turn on</b> notifications for this app?
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="text-lg mb-4">
|
|
||||||
Waiting for system initialization, which may take up to 10
|
|
||||||
seconds...
|
|
||||||
<fa icon="spinner" spin />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="serviceWorkerReady"
|
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
@click="
|
|
||||||
close(notification.id);
|
|
||||||
turnOnNotifications();
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
Turn on Notifications
|
Turn on Notifications
|
||||||
</button>
|
</button>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<button
|
<button
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Maybe Later
|
Maybe Later
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Never
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,10 +233,6 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="
|
|
||||||
close(notification.id);
|
|
||||||
turnOffNotifications();
|
|
||||||
"
|
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
Turn Off Notifications
|
Turn Off Notifications
|
||||||
@@ -262,360 +254,4 @@
|
|||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts"></script>
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
|
||||||
import axios from "axios";
|
|
||||||
interface ServiceWorkerMessage {
|
|
||||||
type: string;
|
|
||||||
data: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ServiceWorkerResponse {
|
|
||||||
// Define the properties and their types
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example interface for error
|
|
||||||
interface ErrorResponse {
|
|
||||||
message: string;
|
|
||||||
// Other properties as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VapidResponse {
|
|
||||||
data: {
|
|
||||||
vapidKey: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
|
||||||
import { db } from "@/db/index";
|
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
||||||
import { sendTestThroughPushServer } from "@/libs/util";
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
group: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class App extends Vue {
|
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
b64 = "";
|
|
||||||
serviceWorkerReady = false;
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
|
||||||
if (settings?.webPushServer) {
|
|
||||||
pushUrl = settings.webPushServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios
|
|
||||||
.get(pushUrl + "/web-push/vapid")
|
|
||||||
.then((response: VapidResponse) => {
|
|
||||||
this.b64 = response.data?.vapidKey || "";
|
|
||||||
console.log("Got vapid key:", this.b64);
|
|
||||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
|
||||||
console.log("New service worker is now controlling the page");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (!this.b64) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Setting Notifications",
|
|
||||||
text: "Could not set notifications.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (window.location.host.startsWith("localhost")) {
|
|
||||||
console.log("Ignoring the error getting VAPID for local development.");
|
|
||||||
} else {
|
|
||||||
console.error("Got an error initializing notifications:", error);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Setting Notifications",
|
|
||||||
text: "Got an error setting notifications.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// there may be a long pause here on first initialization
|
|
||||||
navigator.serviceWorker.ready.then(() => {
|
|
||||||
this.serviceWorkerReady = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendMessageToServiceWorker(
|
|
||||||
message: ServiceWorkerMessage,
|
|
||||||
): Promise<unknown> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
const messageChannel = new MessageChannel();
|
|
||||||
|
|
||||||
messageChannel.port1.onmessage = (event: MessageEvent) => {
|
|
||||||
if (event.data.error) {
|
|
||||||
reject(event.data.error as ErrorResponse);
|
|
||||||
} else {
|
|
||||||
resolve(event.data as ServiceWorkerResponse);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
navigator.serviceWorker.controller.postMessage(message, [
|
|
||||||
messageChannel.port2,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
reject("Service worker controller not available");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private askPermission(): Promise<NotificationPermission> {
|
|
||||||
console.log("Requesting permission for notifications:", navigator);
|
|
||||||
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
|
|
||||||
return Promise.reject("Service worker not available.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const secret = localStorage.getItem("secret");
|
|
||||||
if (!secret) {
|
|
||||||
return Promise.reject("No secret found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.sendSecretToServiceWorker(secret)
|
|
||||||
.then(() => this.checkNotificationSupport())
|
|
||||||
.then(() => this.requestNotificationPermission())
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendSecretToServiceWorker(secret: string): Promise<void> {
|
|
||||||
const message: ServiceWorkerMessage = {
|
|
||||||
type: "SEND_LOCAL_DATA",
|
|
||||||
data: secret,
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.sendMessageToServiceWorker(message).then((response) => {
|
|
||||||
console.log("Response from service worker:", response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkNotificationSupport(): Promise<void> {
|
|
||||||
if (!("Notification" in window)) {
|
|
||||||
alert("This browser does not support notifications.");
|
|
||||||
return Promise.reject("This browser does not support notifications.");
|
|
||||||
}
|
|
||||||
if (Notification.permission === "granted") {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
private requestNotificationPermission(): Promise<NotificationPermission> {
|
|
||||||
return Notification.requestPermission().then((permission) => {
|
|
||||||
if (permission !== "granted") {
|
|
||||||
alert(
|
|
||||||
"Allow this app permission to make notifications for personal reminders." +
|
|
||||||
" You can adjust them at any time in your settings.",
|
|
||||||
);
|
|
||||||
throw new Error("We weren't granted permission.");
|
|
||||||
}
|
|
||||||
return permission;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async turnOnNotifications() {
|
|
||||||
return this.askPermission()
|
|
||||||
.then((permission) => {
|
|
||||||
console.log("Permission granted:", permission);
|
|
||||||
|
|
||||||
// Call the function and handle promises
|
|
||||||
this.subscribeToPush()
|
|
||||||
.then(() => {
|
|
||||||
console.log("Subscribed successfully.");
|
|
||||||
return navigator.serviceWorker.ready;
|
|
||||||
})
|
|
||||||
.then((registration) => {
|
|
||||||
return registration.pushManager.getSubscription();
|
|
||||||
})
|
|
||||||
.then(async (subscription) => {
|
|
||||||
if (subscription) {
|
|
||||||
await this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "info",
|
|
||||||
title: "Notification Setup Underway",
|
|
||||||
text: "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
this.sendSubscriptionToServer(subscription);
|
|
||||||
return subscription;
|
|
||||||
} else {
|
|
||||||
throw new Error("Subscription object is not available.");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(async (subscription) => {
|
|
||||||
console.log(
|
|
||||||
"Subscription data sent to server and all finished successfully.",
|
|
||||||
);
|
|
||||||
await sendTestThroughPushServer(subscription, true);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Notifications Turned On",
|
|
||||||
text: "Notifications are on. You should see at least one on your device; if not, check the 'Troubleshoot' page.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(
|
|
||||||
"Subscription or server communication failed:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
alert(
|
|
||||||
"Subscription or server communication failed. Try again in a while.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(
|
|
||||||
"An error occurred setting notification permissions:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
alert("Some error occurred setting notification permissions.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
||||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
||||||
const base64 = (base64String + padding)
|
|
||||||
.replace(/-/g, "+")
|
|
||||||
.replace(/_/g, "/");
|
|
||||||
const rawData = window.atob(base64);
|
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return outputArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
private subscribeToPush(): Promise<void> {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
if (!("serviceWorker" in navigator && "PushManager" in window)) {
|
|
||||||
const errorMsg = "Push messaging is not supported";
|
|
||||||
console.warn(errorMsg);
|
|
||||||
return reject(new Error(errorMsg));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Notification.permission !== "granted") {
|
|
||||||
const errorMsg = "Notification permission not granted";
|
|
||||||
console.warn(errorMsg);
|
|
||||||
return reject(new Error(errorMsg));
|
|
||||||
}
|
|
||||||
|
|
||||||
const applicationServerKey = this.urlBase64ToUint8Array(this.b64);
|
|
||||||
const options: PushSubscriptionOptions = {
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: applicationServerKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
navigator.serviceWorker.ready
|
|
||||||
.then((registration) => {
|
|
||||||
return registration.pushManager.subscribe(options);
|
|
||||||
})
|
|
||||||
.then((subscription) => {
|
|
||||||
console.log("Push subscription successful:", subscription);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Push subscription failed:", error, options);
|
|
||||||
|
|
||||||
// Inform the user about the issue
|
|
||||||
alert(
|
|
||||||
"We encountered an issue setting up push notifications. " +
|
|
||||||
"If you wish to revoke notification permissions, please do so in your browser settings.",
|
|
||||||
);
|
|
||||||
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendSubscriptionToServer(
|
|
||||||
subscription: PushSubscription,
|
|
||||||
): Promise<void> {
|
|
||||||
console.log("About to send subscription...", subscription);
|
|
||||||
return fetch("/web-push/subscribe", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(subscription),
|
|
||||||
}).then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to send subscription to server");
|
|
||||||
}
|
|
||||||
console.log("Subscription sent to server successfully.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async turnOffNotifications() {
|
|
||||||
let subscription;
|
|
||||||
const pushProviderSuccess = await navigator.serviceWorker.ready
|
|
||||||
.then((registration) => {
|
|
||||||
return registration.pushManager.getSubscription();
|
|
||||||
})
|
|
||||||
.then((subscript) => {
|
|
||||||
subscription = subscript;
|
|
||||||
if (subscription) {
|
|
||||||
return subscription.unsubscribe();
|
|
||||||
} else {
|
|
||||||
console.log("Subscription object is not available.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log("Push provider server communication failed:", error);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(subscription),
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
return response.ok;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log("Push server communication failed:", error);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
alert(
|
|
||||||
"Notifications are off. Push provider unsubscribe " +
|
|
||||||
(pushProviderSuccess ? "succeeded" : "failed") +
|
|
||||||
(pushProviderSuccess === pushServerSuccess ? " and" : " but") +
|
|
||||||
" push server unsubscribe " +
|
|
||||||
(pushServerSuccess ? "succeeded" : "failed") +
|
|
||||||
".",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" enable-background="new 0 0 50 50">
|
|
||||||
<path d="M30.3 13.7L25 8.4l-5.3 5.3-1.4-1.4L25 5.6l6.7 6.7z"/><path d="M24 7h2v21h-2z"/><path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 365 B |
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
||||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="64px" height="64px" viewBox="5.5 -3.5 64 64" enable-background="new 5.5 -3.5 64 64" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<circle fill="#FFFFFF" cx="37.785" cy="28.501" r="28.836"/>
|
|
||||||
<path d="M37.441-3.5c8.951,0,16.572,3.125,22.857,9.372c3.008,3.009,5.295,6.448,6.857,10.314
|
|
||||||
c1.561,3.867,2.344,7.971,2.344,12.314c0,4.381-0.773,8.486-2.314,12.313c-1.543,3.828-3.82,7.21-6.828,10.143
|
|
||||||
c-3.123,3.085-6.666,5.448-10.629,7.086c-3.961,1.638-8.057,2.457-12.285,2.457s-8.276-0.808-12.143-2.429
|
|
||||||
c-3.866-1.618-7.333-3.961-10.4-7.027c-3.067-3.066-5.4-6.524-7-10.372S5.5,32.767,5.5,28.5c0-4.229,0.809-8.295,2.428-12.2
|
|
||||||
c1.619-3.905,3.972-7.4,7.057-10.486C21.08-0.394,28.565-3.5,37.441-3.5z M37.557,2.272c-7.314,0-13.467,2.553-18.458,7.657
|
|
||||||
c-2.515,2.553-4.448,5.419-5.8,8.6c-1.354,3.181-2.029,6.505-2.029,9.972c0,3.429,0.675,6.734,2.029,9.913
|
|
||||||
c1.353,3.183,3.285,6.021,5.8,8.516c2.514,2.496,5.351,4.399,8.515,5.715c3.161,1.314,6.476,1.971,9.943,1.971
|
|
||||||
c3.428,0,6.75-0.665,9.973-1.999c3.219-1.335,6.121-3.257,8.713-5.771c4.99-4.876,7.484-10.99,7.484-18.344
|
|
||||||
c0-3.543-0.648-6.895-1.943-10.057c-1.293-3.162-3.18-5.98-5.654-8.458C50.984,4.844,44.795,2.272,37.557,2.272z M37.156,23.187
|
|
||||||
l-4.287,2.229c-0.458-0.951-1.019-1.619-1.685-2c-0.667-0.38-1.286-0.571-1.858-0.571c-2.856,0-4.286,1.885-4.286,5.657
|
|
||||||
c0,1.714,0.362,3.084,1.085,4.113c0.724,1.029,1.791,1.544,3.201,1.544c1.867,0,3.181-0.915,3.944-2.743l3.942,2
|
|
||||||
c-0.838,1.563-2,2.791-3.486,3.686c-1.484,0.896-3.123,1.343-4.914,1.343c-2.857,0-5.163-0.875-6.915-2.629
|
|
||||||
c-1.752-1.752-2.628-4.19-2.628-7.313c0-3.048,0.886-5.466,2.657-7.257c1.771-1.79,4.009-2.686,6.715-2.686
|
|
||||||
C32.604,18.558,35.441,20.101,37.156,23.187z M55.613,23.187l-4.229,2.229c-0.457-0.951-1.02-1.619-1.686-2
|
|
||||||
c-0.668-0.38-1.307-0.571-1.914-0.571c-2.857,0-4.287,1.885-4.287,5.657c0,1.714,0.363,3.084,1.086,4.113
|
|
||||||
c0.723,1.029,1.789,1.544,3.201,1.544c1.865,0,3.18-0.915,3.941-2.743l4,2c-0.875,1.563-2.057,2.791-3.541,3.686
|
|
||||||
c-1.486,0.896-3.105,1.343-4.857,1.343c-2.896,0-5.209-0.875-6.941-2.629c-1.736-1.752-2.602-4.19-2.602-7.313
|
|
||||||
c0-3.048,0.885-5.466,2.658-7.257c1.77-1.79,4.008-2.686,6.713-2.686C51.117,18.558,53.938,20.101,55.613,23.187z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="64px" height="64px" viewBox="-0.5 0.5 64 64" enable-background="new -0.5 0.5 64 64" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<circle fill="#FFFFFF" cx="31.325" cy="32.873" r="30.096"/>
|
|
||||||
<path id="text2809_1_" d="M31.5,14.08c-10.565,0-13.222,9.969-13.222,18.42c0,8.452,2.656,18.42,13.222,18.42
|
|
||||||
c10.564,0,13.221-9.968,13.221-18.42C44.721,24.049,42.064,14.08,31.5,14.08z M31.5,21.026c0.429,0,0.82,0.066,1.188,0.157
|
|
||||||
c0.761,0.656,1.133,1.561,0.403,2.823l-7.036,12.93c-0.216-1.636-0.247-3.24-0.247-4.437C25.808,28.777,26.066,21.026,31.5,21.026z
|
|
||||||
M36.766,26.987c0.373,1.984,0.426,4.056,0.426,5.513c0,3.723-0.258,11.475-5.69,11.475c-0.428,0-0.822-0.045-1.188-0.136
|
|
||||||
c-0.07-0.021-0.134-0.043-0.202-0.067c-0.112-0.032-0.23-0.068-0.336-0.11c-1.21-0.515-1.972-1.446-0.874-3.093L36.766,26.987z"/>
|
|
||||||
<path id="path2815_1_" d="M31.433,0.5c-8.877,0-16.359,3.09-22.454,9.3c-3.087,3.087-5.443,6.607-7.082,10.532
|
|
||||||
C0.297,24.219-0.5,28.271-0.5,32.5c0,4.268,0.797,8.32,2.397,12.168c1.6,3.85,3.921,7.312,6.969,10.396
|
|
||||||
c3.085,3.049,6.549,5.399,10.398,7.037c3.886,1.602,7.939,2.398,12.169,2.398c4.229,0,8.34-0.826,12.303-2.465
|
|
||||||
c3.962-1.639,7.496-3.994,10.621-7.081c3.011-2.933,5.289-6.297,6.812-10.106C62.73,41,63.5,36.883,63.5,32.5
|
|
||||||
c0-4.343-0.77-8.454-2.33-12.303c-1.562-3.885-3.848-7.32-6.857-10.33C48.025,3.619,40.385,0.5,31.433,0.5z M31.567,6.259
|
|
||||||
c7.238,0,13.412,2.566,18.554,7.709c2.477,2.477,4.375,5.31,5.67,8.471c1.296,3.162,1.949,6.518,1.949,10.061
|
|
||||||
c0,7.354-2.516,13.454-7.506,18.33c-2.592,2.516-5.502,4.447-8.74,5.781c-3.2,1.334-6.498,1.994-9.927,1.994
|
|
||||||
c-3.468,0-6.788-0.653-9.949-1.948c-3.163-1.334-6.001-3.238-8.516-5.716c-2.515-2.514-4.455-5.353-5.826-8.516
|
|
||||||
c-1.333-3.199-2.017-6.498-2.017-9.927c0-3.467,0.684-6.787,2.017-9.949c1.371-3.2,3.312-6.074,5.826-8.628
|
|
||||||
C18.092,8.818,24.252,6.259,31.567,6.259z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -10,24 +10,21 @@
|
|||||||
placeholder="What was received"
|
placeholder="What was received"
|
||||||
v-model="description"
|
v-model="description"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row mb-6">
|
||||||
<span
|
<span
|
||||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2"
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2"
|
||||||
@click="changeUnitCode()"
|
>Hours</span
|
||||||
>
|
>
|
||||||
{{ UNIT_SHORT[unitCode] }}
|
|
||||||
</span>
|
|
||||||
<div
|
<div
|
||||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
@click="decrement()"
|
@click="decrement()"
|
||||||
v-if="amountInput !== '0'"
|
|
||||||
>
|
>
|
||||||
<fa icon="chevron-left" />
|
<fa icon="chevron-left" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
||||||
v-model="amountInput"
|
v-model="hours"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||||
@@ -36,13 +33,7 @@
|
|||||||
<fa icon="chevron-right" />
|
<fa icon="chevron-right" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showGivenToUser" class="mt-2 text-right">
|
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
|
||||||
<input type="checkbox" class="mr-2" v-model="givenToUser" />
|
|
||||||
<label class="text-sm">Given to you</label>
|
|
||||||
</div>
|
|
||||||
<p class="text-center mb-2 mt-6 italic">
|
|
||||||
Sign & Send to publish to the world
|
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||||
@click="confirm"
|
@click="confirm"
|
||||||
@@ -79,36 +70,15 @@ export default class GiftedDialog extends Vue {
|
|||||||
|
|
||||||
@Prop message = "";
|
@Prop message = "";
|
||||||
@Prop projectId = "";
|
@Prop projectId = "";
|
||||||
@Prop showGivenToUser = false;
|
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
|
||||||
amountInput = "0";
|
giver?: GiverInputInfo;
|
||||||
giver?: GiverInputInfo; // undefined means no identified giver agent
|
|
||||||
description = "";
|
description = "";
|
||||||
givenToUser = false;
|
hours = "0";
|
||||||
unitCode = "HUR";
|
|
||||||
visible = false;
|
visible = false;
|
||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
|
||||||
UNIT_SHORT: Record<string, string> = {
|
|
||||||
"BTC": "BTC",
|
|
||||||
"ETH": "ETH",
|
|
||||||
"HUR": "Hours",
|
|
||||||
"USD": "US $",
|
|
||||||
};
|
|
||||||
/* eslint-enable prettier/prettier */
|
|
||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
|
||||||
UNIT_LONG: Record<string, string> = {
|
|
||||||
"BTC": "BTC",
|
|
||||||
"ETH": "ETH",
|
|
||||||
"HUR": "hours",
|
|
||||||
"USD": "dollars",
|
|
||||||
};
|
|
||||||
/* eslint-enable prettier/prettier */
|
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -123,7 +93,9 @@ export default class GiftedDialog extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: err.message || "There was an error retrieving your settings.",
|
text:
|
||||||
|
err.message ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -131,47 +103,27 @@ export default class GiftedDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(giver: GiverInputInfo) {
|
open(giver: GiverInputInfo) {
|
||||||
this.description = "";
|
|
||||||
this.giver = giver;
|
this.giver = giver;
|
||||||
// if we show "given to user" selection, default checkbox to true
|
|
||||||
this.givenToUser = this.showGivenToUser;
|
|
||||||
this.amountInput = "0";
|
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
// close the dialog but don't change values (since it might be submitting info)
|
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeUnitCode() {
|
|
||||||
const units = Object.keys(this.UNIT_SHORT);
|
|
||||||
const index = units.indexOf(this.unitCode);
|
|
||||||
this.unitCode = units[(index + 1) % units.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
decrement() {
|
decrement() {
|
||||||
this.amountInput = `${Math.max(
|
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||||
0,
|
|
||||||
(parseFloat(this.amountInput) || 1) - 1,
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.close();
|
this.close();
|
||||||
this.eraseValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
eraseValues() {
|
|
||||||
this.description = "";
|
this.description = "";
|
||||||
this.giver = undefined;
|
this.giver = undefined;
|
||||||
this.givenToUser = this.showGivenToUser;
|
this.hours = "0";
|
||||||
this.amountInput = "0";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm() {
|
async confirm() {
|
||||||
@@ -186,13 +138,14 @@ export default class GiftedDialog extends Vue {
|
|||||||
1000,
|
1000,
|
||||||
);
|
);
|
||||||
// this is asynchronous, but we don't need to wait for it to complete
|
// this is asynchronous, but we don't need to wait for it to complete
|
||||||
await this.recordGive(
|
this.recordGive(
|
||||||
this.giver?.did as string | undefined,
|
this.giver?.did as string | undefined,
|
||||||
this.description,
|
this.description,
|
||||||
parseFloat(this.amountInput),
|
parseFloat(this.hours),
|
||||||
this.unitCode,
|
|
||||||
).then(() => {
|
).then(() => {
|
||||||
this.eraseValues();
|
this.description = "";
|
||||||
|
this.giver = undefined;
|
||||||
|
this.hours = "0";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,13 +169,12 @@ export default class GiftedDialog extends Vue {
|
|||||||
*
|
*
|
||||||
* @param giverDid may be null
|
* @param giverDid may be null
|
||||||
* @param description may be an empty string
|
* @param description may be an empty string
|
||||||
* @param amountInput may be 0
|
* @param hours may be 0
|
||||||
*/
|
*/
|
||||||
public async recordGive(
|
public async recordGive(
|
||||||
giverDid?: string,
|
giverDid?: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
amountInput?: number,
|
hours?: number,
|
||||||
unitCode?: string,
|
|
||||||
) {
|
) {
|
||||||
if (!this.activeDid) {
|
if (!this.activeDid) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -237,15 +189,13 @@ export default class GiftedDialog extends Vue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description && !amountInput) {
|
if (!description && !hours) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: `You must enter a description or some number of ${
|
text: "You must enter a description or some number of hours.",
|
||||||
this.UNIT_LONG[this.unitCode]
|
|
||||||
}.`,
|
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -259,10 +209,9 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.apiServer,
|
this.apiServer,
|
||||||
identity,
|
identity,
|
||||||
giverDid,
|
giverDid,
|
||||||
this.givenToUser ? this.activeDid : undefined,
|
this.activeDid,
|
||||||
description,
|
description,
|
||||||
amountInput,
|
hours,
|
||||||
unitCode,
|
|
||||||
this.projectId,
|
this.projectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -289,7 +238,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
title: "Success",
|
title: "Success",
|
||||||
text: "That gift was recorded.",
|
text: "That gift was recorded.",
|
||||||
},
|
},
|
||||||
7000,
|
10000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -1,315 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="visible" class="dialog-overlay">
|
|
||||||
<div class="dialog">
|
|
||||||
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
||||||
placeholder="Description, prerequisites, terms, etc."
|
|
||||||
v-model="description"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-row mb-6">
|
|
||||||
<span
|
|
||||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
|
||||||
>
|
|
||||||
Hours
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
|
||||||
@click="decrement()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
|
||||||
v-model="hours"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
|
||||||
@click="increment()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-right" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row mb-6">
|
|
||||||
<span
|
|
||||||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2"
|
|
||||||
>
|
|
||||||
Expiration
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="w-full border border-slate-400 px-2 py-2 rounded-r"
|
|
||||||
:placeholder="'Date, eg. ' + new Date().toISOString().slice(0, 10)"
|
|
||||||
v-model="expirationDateInput"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
|
|
||||||
<button
|
|
||||||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
|
||||||
@click="confirm"
|
|
||||||
>
|
|
||||||
Sign & Send
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
|
||||||
@click="cancel"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
|
||||||
import { createAndSubmitOffer } from "@/libs/endorserServer";
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
group: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class OfferDialog extends Vue {
|
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
@Prop message = "";
|
|
||||||
@Prop projectId = "";
|
|
||||||
|
|
||||||
activeDid = "";
|
|
||||||
apiServer = "";
|
|
||||||
|
|
||||||
description = "";
|
|
||||||
expirationDateInput = "";
|
|
||||||
hours = "0";
|
|
||||||
visible = false;
|
|
||||||
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
|
||||||
this.apiServer = settings?.apiServer || "";
|
|
||||||
this.activeDid = settings?.activeDid || "";
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (err: any) {
|
|
||||||
console.log("Error retrieving settings from database:", err);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: err.message || "There was an error retrieving your settings.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open() {
|
|
||||||
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)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.close();
|
|
||||||
this.description = "";
|
|
||||||
this.hours = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
async confirm() {
|
|
||||||
this.close();
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "toast",
|
|
||||||
text: "Recording the offer...",
|
|
||||||
title: "",
|
|
||||||
},
|
|
||||||
1000,
|
|
||||||
);
|
|
||||||
// this is asynchronous, but we don't need to wait for it to complete
|
|
||||||
this.recordOffer(
|
|
||||||
this.description,
|
|
||||||
parseFloat(this.hours),
|
|
||||||
this.expirationDateInput,
|
|
||||||
).then(() => {
|
|
||||||
this.description = "";
|
|
||||||
this.hours = "0";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
|
||||||
await accountsDB.open();
|
|
||||||
const account = (await accountsDB.accounts
|
|
||||||
.where("did")
|
|
||||||
.equals(activeDid)
|
|
||||||
.first()) as Account;
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error(
|
|
||||||
"Attempted to load Offer records for DID ${activeDid} but no identity was found",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param description may be an empty string
|
|
||||||
* @param hours may be 0
|
|
||||||
*/
|
|
||||||
public async recordOffer(
|
|
||||||
description?: string,
|
|
||||||
hours?: number,
|
|
||||||
expirationDateInput?: string,
|
|
||||||
) {
|
|
||||||
if (!this.activeDid) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "You must select an identity before you can record an offer.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!description && !hours) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "You must enter a description or some number of hours.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
|
||||||
const result = await createAndSubmitOffer(
|
|
||||||
this.axios,
|
|
||||||
this.apiServer,
|
|
||||||
identity,
|
|
||||||
description,
|
|
||||||
hours,
|
|
||||||
expirationDateInput,
|
|
||||||
this.projectId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
result.type === "error" ||
|
|
||||||
this.isOfferCreationError(result.response)
|
|
||||||
) {
|
|
||||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
|
||||||
console.log("Error with offer creation result:", result);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: errorMessage || "There was an error creating the offer.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Success",
|
|
||||||
text: "That offer was recorded.",
|
|
||||||
},
|
|
||||||
10000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log("Error with offer recordation caught:", error);
|
|
||||||
const message =
|
|
||||||
error.userMessage ||
|
|
||||||
error.response?.data?.error?.message ||
|
|
||||||
"There was an error recording the offer.";
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: message,
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for readability
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result response "data" from the server
|
|
||||||
* @returns true if the result indicates an error
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
isOfferCreationError(result: any) {
|
|
||||||
return result.status !== 201 || result.data?.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
|
||||||
* @returns best guess at an error message
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
getOfferCreationErrorMessage(result: any) {
|
|
||||||
return (
|
|
||||||
result.error?.userMessage ||
|
|
||||||
result.error?.error ||
|
|
||||||
result.response?.data?.error?.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- QUICK NAV -->
|
<!-- QUICK NAV -->
|
||||||
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
<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 max-w-3xl mx-auto">
|
<ul class="flex text-2xl p-2 gap-2">
|
||||||
<!-- Home Feed -->
|
<!-- Home Feed -->
|
||||||
<li
|
<li
|
||||||
:class="{
|
:class="{
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="text-center text-red-500">{{ message }}</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
|
||||||
import { db } from "@/db/index";
|
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
||||||
import { AppString } from "@/constants/app";
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
group: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class TopMessage extends Vue {
|
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
@Prop selected = "";
|
|
||||||
|
|
||||||
message = "";
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
if (
|
|
||||||
settings?.warnIfTestServer &&
|
|
||||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
|
||||||
) {
|
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
|
||||||
this.message = "You're linked to a non-prod server, user " + didPrefix;
|
|
||||||
} else if (
|
|
||||||
settings?.warnIfProdServer &&
|
|
||||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
|
||||||
) {
|
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
|
||||||
this.message =
|
|
||||||
"You're linked to the production server, user " + didPrefix;
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Detecting Server",
|
|
||||||
text: JSON.stringify(err),
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -4,26 +4,20 @@
|
|||||||
* See also ../libs/veramo/setup.ts
|
* See also ../libs/veramo/setup.ts
|
||||||
*/
|
*/
|
||||||
export enum AppString {
|
export enum AppString {
|
||||||
|
APP_NAME = "Time Safari",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||||
|
|
||||||
PROD_PUSH_SERVER = "https://timesafari.app",
|
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
||||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
|
||||||
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_ENDORSER_API_SERVER = AppString.TEST_ENDORSER_API_SERVER;
|
|
||||||
|
|
||||||
export const DEFAULT_PUSH_SERVER =
|
|
||||||
window.location.protocol + "//" + window.location.host;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible values for "group" and "type" are in App.vue.
|
* See notiwind package
|
||||||
* From the notiwind package
|
|
||||||
*/
|
*/
|
||||||
export interface NotificationIface {
|
export interface NotificationIface {
|
||||||
group: string; // "alert" | "modal"
|
group: string;
|
||||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
|||||||
@@ -1,57 +1,94 @@
|
|||||||
import BaseDexie, { Table } from "dexie";
|
import BaseDexie, { Table } from "dexie";
|
||||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||||
import { Account, AccountsSchema } from "./tables/accounts";
|
import { Account, AccountsSchema } from "./tables/accounts";
|
||||||
import { Contact, ContactSchema } from "./tables/contacts";
|
import { Contact, ContactsSchema } from "./tables/contacts";
|
||||||
import { Log, LogSchema } from "./tables/logs";
|
|
||||||
import {
|
import {
|
||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
Settings,
|
Settings,
|
||||||
SettingsSchema,
|
SettingsSchema,
|
||||||
|
SettingsSchemaV1,
|
||||||
} from "./tables/settings";
|
} from "./tables/settings";
|
||||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
|
|
||||||
|
// a separate DB because the seed is super-sensitive data
|
||||||
|
type SensitiveTables = {
|
||||||
|
accounts: Table<Account>;
|
||||||
|
};
|
||||||
|
|
||||||
// Define types for tables that hold sensitive and non-sensitive data
|
|
||||||
type SensitiveTables = { accounts: Table<Account> };
|
|
||||||
type NonsensitiveTables = {
|
type NonsensitiveTables = {
|
||||||
contacts: Table<Contact>;
|
contacts: Table<Contact>;
|
||||||
logs: Table<Log>;
|
|
||||||
settings: Table<Settings>;
|
settings: Table<Settings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
/**
|
||||||
|
* In order to make the next line be acceptable, the program needs to have its linter suppress a rule:
|
||||||
|
* https://typescript-eslint.io/rules/no-unnecessary-type-constraint/
|
||||||
|
*
|
||||||
|
* and change *any* to *unknown*
|
||||||
|
*
|
||||||
|
* https://9to5answer.com/how-to-bypass-warning-unexpected-any-specify-a-different-type-typescript-eslint-no-explicit-any
|
||||||
|
*/
|
||||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||||
|
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||||
|
const SensitiveSchemas = Object.assign({}, AccountsSchema);
|
||||||
|
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||||
BaseDexie & T;
|
BaseDexie & T;
|
||||||
|
|
||||||
// Initialize Dexie databases for sensitive and non-sensitive data
|
|
||||||
export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
|
||||||
const SensitiveSchemas = { ...AccountsSchema };
|
|
||||||
|
|
||||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
const NonsensitiveSchemas = {
|
// eslint-disable-next-line prettier/prettier
|
||||||
...ContactSchema,
|
const NonsensitiveSchemasV1 = Object.assign({}, ContactsSchema, SettingsSchemaV1);
|
||||||
...LogSchema,
|
const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
|
||||||
...SettingsSchema,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Manage the encryption key. If not present in localStorage, create and store it.
|
/**
|
||||||
|
* Needed to enable a special webpack setting to allow *await* below:
|
||||||
|
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 =
|
const secret =
|
||||||
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
||||||
if (!localStorage.getItem("secret")) localStorage.setItem("secret", secret);
|
|
||||||
|
|
||||||
// Apply encryption to the sensitive database using the secret key
|
if (localStorage.getItem("secret") == null) {
|
||||||
|
localStorage.setItem("secret", secret);
|
||||||
|
}
|
||||||
|
|
||||||
encrypted(accountsDB, { secretKey: secret });
|
encrypted(accountsDB, { secretKey: secret });
|
||||||
|
|
||||||
// Define the schema for our databases
|
|
||||||
accountsDB.version(1).stores(SensitiveSchemas);
|
accountsDB.version(1).stores(SensitiveSchemas);
|
||||||
// v1 was contacts & settings
|
|
||||||
// v2 added logs
|
|
||||||
db.version(2).stores(NonsensitiveSchemas);
|
|
||||||
|
|
||||||
// Event handler to initialize the non-sensitive database with default settings
|
db.version(1).stores(NonsensitiveSchemasV1);
|
||||||
db.on("populate", () => {
|
|
||||||
|
db.version(2)
|
||||||
|
.stores(NonsensitiveSchemas)
|
||||||
|
.upgrade((tx) => {
|
||||||
|
return tx
|
||||||
|
.table("settings")
|
||||||
|
.toCollection()
|
||||||
|
.modify((settings) => {
|
||||||
|
if (
|
||||||
|
typeof settings.firstName === "string" &&
|
||||||
|
typeof settings.lastName === "string"
|
||||||
|
) {
|
||||||
|
settings.firstName += " " + settings.lastName;
|
||||||
|
} else if (typeof settings.lastName === "string") {
|
||||||
|
settings.firstName = settings.lastName;
|
||||||
|
}
|
||||||
|
delete settings.lastName;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("caught modify exception", e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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({
|
db.settings.add({
|
||||||
id: MASTER_SETTINGS_KEY,
|
id: MASTER_SETTINGS_KEY,
|
||||||
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
export interface Contact {
|
export interface Contact {
|
||||||
did: string;
|
did: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
|
||||||
publicKeyBase64?: string;
|
publicKeyBase64?: string;
|
||||||
seesMe?: boolean;
|
seesMe?: boolean;
|
||||||
registered?: boolean;
|
registered?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactSchema = {
|
export const ContactsSchema = {
|
||||||
contacts: "&did, name", // no need to key by other things
|
contacts: "&did, name, publicKeyBase64, registered, seesMe",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
export interface Log {
|
|
||||||
date: string;
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LogSchema = {
|
|
||||||
// Currently keyed by "date" because A) today's log data is what we need so we append, and
|
|
||||||
// B) we don't want it to grow so we remove everything if this is the first entry today.
|
|
||||||
// See safari-notifications.js logMessage for the associated logic.
|
|
||||||
logs: "date", // definitely don't key by the potentially large message field
|
|
||||||
};
|
|
||||||
@@ -1,50 +1,34 @@
|
|||||||
/**
|
|
||||||
* BoundingBox type describes the geographical bounding box coordinates.
|
|
||||||
*/
|
|
||||||
export type BoundingBox = {
|
export type BoundingBox = {
|
||||||
eastLong: number; // Eastern longitude
|
eastLong: number;
|
||||||
maxLat: number; // Maximum (Northernmost) latitude
|
maxLat: number;
|
||||||
minLat: number; // Minimum (Southernmost) latitude
|
minLat: number;
|
||||||
westLong: number; // Western longitude
|
westLong: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// a singleton
|
||||||
* Settings type encompasses user-specific configuration details.
|
|
||||||
*/
|
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
id: number; // Only one entry, keyed with MASTER_SETTINGS_KEY
|
id: number; // there's only one entry: MASTER_SETTINGS_KEY
|
||||||
|
|
||||||
activeDid?: string; // Active Decentralized ID
|
activeDid?: string;
|
||||||
apiServer?: string; // API server URL
|
apiServer?: string;
|
||||||
firstName?: string; // User's first name
|
firstName?: string;
|
||||||
isRegistered?: boolean;
|
isRegistered?: boolean;
|
||||||
lastName?: string; // deprecated - put all names in firstName
|
lastName?: string; // deprecated, pre v 0.1.3
|
||||||
lastNotifiedClaimId?: string; // Last notified claim ID
|
lastViewedClaimId?: string;
|
||||||
lastViewedClaimId?: string; // Last viewed claim ID
|
|
||||||
reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders
|
|
||||||
reminderOn?: boolean; // Toggle to enable or disable reminders
|
|
||||||
|
|
||||||
// Array of named search boxes defined by bounding boxes
|
|
||||||
searchBoxes?: Array<{
|
searchBoxes?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
bbox: BoundingBox;
|
bbox: BoundingBox;
|
||||||
}>;
|
}>;
|
||||||
|
showContactGivesInline?: boolean;
|
||||||
showContactGivesInline?: boolean; // Display contact inline or not
|
|
||||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
|
||||||
warnIfProdServer?: boolean; // Warn if using a production server
|
|
||||||
warnIfTestServer?: boolean; // Warn if using a testing server
|
|
||||||
webPushServer?: string; // Web Push server URL
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const SettingsSchemaV1 = {
|
||||||
* Schema for the Settings table in the database.
|
|
||||||
*/
|
|
||||||
export const SettingsSchema = {
|
|
||||||
settings: "id",
|
settings: "id",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const SettingsSchema = {
|
||||||
* Constants.
|
settings:
|
||||||
*/
|
"id, activeDid, apiServer, firstName, lastname, lastViewedClaimId, searchBoxes, showContactGivesInline",
|
||||||
|
};
|
||||||
|
|
||||||
export const MASTER_SETTINGS_KEY = 1;
|
export const MASTER_SETTINGS_KEY = 1;
|
||||||
|
|||||||
@@ -173,19 +173,3 @@ export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
|
|||||||
|
|
||||||
return jwt.payload;
|
return jwt.payload;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const nextDerivationPath = (origDerivPath: string) => {
|
|
||||||
let lastStr = origDerivPath.split("/").slice(-1)[0];
|
|
||||||
if (lastStr.endsWith("'")) {
|
|
||||||
lastStr = lastStr.slice(0, -1);
|
|
||||||
}
|
|
||||||
const lastNum = parseInt(lastStr, 10);
|
|
||||||
const newLastNum = lastNum + 1;
|
|
||||||
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : "");
|
|
||||||
const newDerivPath = origDerivPath
|
|
||||||
.split("/")
|
|
||||||
.slice(0, -1)
|
|
||||||
.concat([newLastStr])
|
|
||||||
.join("/");
|
|
||||||
return newDerivPath;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ export const SERVICE_ID = "endorser.ch";
|
|||||||
export const CONTACT_URL_PREFIX = "https://endorser.ch";
|
export const CONTACT_URL_PREFIX = "https://endorser.ch";
|
||||||
// the suffix for the contact URL
|
// the suffix for the contact URL
|
||||||
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
|
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
|
||||||
// the prefix for handle IDs, the permanent ID for claims on Endorser
|
|
||||||
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
|
||||||
|
|
||||||
export interface AgreeVerifiableCredential {
|
export interface AgreeVerifiableCredential {
|
||||||
"@context": string;
|
"@context": string;
|
||||||
@@ -40,24 +38,14 @@ export interface ClaimResult {
|
|||||||
error: { code: string; message: string };
|
error: { code: string; message: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericVerifiableCredential {
|
export interface GenericClaim {
|
||||||
"@context": string;
|
"@context": string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
}
|
issuedAt: string;
|
||||||
|
// "any" because arbitrary objects can be subject of agreement
|
||||||
export interface GenericServerRecord extends GenericVerifiableCredential {
|
|
||||||
handleId?: string;
|
|
||||||
id?: string;
|
|
||||||
issuedAt?: string;
|
|
||||||
issuer?: string;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
claim: Record<any, any>;
|
claim: Record<any, any>;
|
||||||
}
|
}
|
||||||
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
|
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
|
||||||
"@type": "",
|
|
||||||
claim: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface GiveServerRecord {
|
export interface GiveServerRecord {
|
||||||
agentDid: string;
|
agentDid: string;
|
||||||
@@ -71,46 +59,17 @@ export interface GiveServerRecord {
|
|||||||
unit: string;
|
unit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfferServerRecord {
|
|
||||||
amount: number;
|
|
||||||
amountGiven: number;
|
|
||||||
offeredByDid: string;
|
|
||||||
recipientDid: string;
|
|
||||||
requirementsMet: boolean;
|
|
||||||
unit: string;
|
|
||||||
validThrough: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
|
||||||
// https://endorser.ch/doc/html/transactions.html#id4
|
|
||||||
export interface GiveVerifiableCredential {
|
export interface GiveVerifiableCredential {
|
||||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||||
"@type": "GiveAction";
|
"@type": string;
|
||||||
agent?: { identifier: string };
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string };
|
fulfills?: { "@type": string; identifier: string };
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
object?: { amountOfThisGood: number; unitCode: string };
|
object?: { amountOfThisGood: number; unitCode: string };
|
||||||
recipient?: { identifier: string };
|
recipient?: { identifier: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
|
||||||
// https://endorser.ch/doc/html/transactions.html#id8
|
|
||||||
export interface OfferVerifiableCredential {
|
|
||||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
|
||||||
"@type": "Offer";
|
|
||||||
description?: string;
|
|
||||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
|
||||||
itemOffered?: {
|
|
||||||
description?: string;
|
|
||||||
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
|
||||||
};
|
|
||||||
offeredBy?: { identifier: string };
|
|
||||||
validThrough?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
|
||||||
// https://endorser.ch/doc/html/transactions.html#id7
|
|
||||||
export interface PlanVerifiableCredential {
|
export interface PlanVerifiableCredential {
|
||||||
"@context": "https://schema.org";
|
"@context": "https://schema.org";
|
||||||
"@type": "PlanAction";
|
"@type": "PlanAction";
|
||||||
@@ -155,104 +114,6 @@ export function isHiddenDid(did: string) {
|
|||||||
return did === HIDDEN_DID;
|
return did === HIDDEN_DID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true for any nested string where func(input) === true
|
|
||||||
*
|
|
||||||
* Similar logic is found in endorser-mobile.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
function testRecursivelyOnString(func: (arg0: any) => boolean, input: any) {
|
|
||||||
if (Object.prototype.toString.call(input) === "[object String]") {
|
|
||||||
return func(input);
|
|
||||||
} else if (input instanceof Object) {
|
|
||||||
if (!Array.isArray(input)) {
|
|
||||||
// it's an object
|
|
||||||
for (const key in input) {
|
|
||||||
if (testRecursivelyOnString(func, input[key])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// it's an array
|
|
||||||
for (const value of input) {
|
|
||||||
if (testRecursivelyOnString(func, value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function containsHiddenDid(obj: any) {
|
|
||||||
return testRecursivelyOnString(isHiddenDid, obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stripEndorserPrefix(claimId: string) {
|
|
||||||
if (claimId && claimId.startsWith(ENDORSER_CH_HANDLE_PREFIX)) {
|
|
||||||
return claimId.substring(ENDORSER_CH_HANDLE_PREFIX.length);
|
|
||||||
} else {
|
|
||||||
return claimId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// similar logic is found in endorser-mobile
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function removeSchemaContext(obj: any) {
|
|
||||||
return obj["@context"] === SCHEMA_ORG_CONTEXT
|
|
||||||
? R.omit(["@context"], obj)
|
|
||||||
: obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
// similar logic is found in endorser-mobile
|
|
||||||
export function addLastClaimOrHandleAsIdIfMissing(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
obj: any,
|
|
||||||
lastClaimId?: string,
|
|
||||||
handleId?: string,
|
|
||||||
) {
|
|
||||||
if (!obj.identifier && lastClaimId) {
|
|
||||||
const result = R.clone(obj);
|
|
||||||
result.lastClaimId = lastClaimId;
|
|
||||||
return result;
|
|
||||||
} else if (!obj.identifier && handleId) {
|
|
||||||
const result = R.clone(obj);
|
|
||||||
result.identifier = handleId;
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return clone of object without any nested *VisibleToDids keys
|
|
||||||
// similar logic is found in endorser-mobile
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function removeVisibleToDids(input: any): any {
|
|
||||||
if (input instanceof Object) {
|
|
||||||
if (!Array.isArray(input)) {
|
|
||||||
// it's an object
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
for (const key in input) {
|
|
||||||
if (!key.endsWith("VisibleToDids")) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
result[key] = removeVisibleToDids(R.clone(input[key]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
// it's an array
|
|
||||||
return R.map(removeVisibleToDids, input);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
|
||||||
|
|
||||||
@@ -273,8 +134,8 @@ export function didInfo(
|
|||||||
return contact
|
return contact
|
||||||
? contact.name || "Contact With No Name"
|
? contact.name || "Contact With No Name"
|
||||||
: isHiddenDid(did)
|
: isHiddenDid(did)
|
||||||
? "Someone Not In Network"
|
? "Someone Not In Network"
|
||||||
: "Someone Not In Contacts";
|
: "Someone Not In Contacts";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultWithType {
|
export interface ResultWithType {
|
||||||
@@ -291,7 +152,7 @@ export interface ErrorResult {
|
|||||||
error: InternalError;
|
error: InternalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
@@ -310,85 +171,21 @@ export async function createAndSubmitGive(
|
|||||||
toDid?: string,
|
toDid?: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
hours?: number,
|
hours?: number,
|
||||||
unitCode?: string,
|
|
||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
): Promise<CreateAndSubmitClaimResult> {
|
): Promise<CreateAndSubmitGiveResult> {
|
||||||
const vcClaim: GiveVerifiableCredential = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "GiveAction",
|
|
||||||
recipient: toDid ? { identifier: toDid } : undefined,
|
|
||||||
agent: fromDid ? { identifier: fromDid } : undefined,
|
|
||||||
description: description || undefined,
|
|
||||||
object: hours
|
|
||||||
? { amountOfThisGood: hours, unitCode: unitCode || "HUR" }
|
|
||||||
: undefined,
|
|
||||||
fulfills: fulfillsProjectHandleId
|
|
||||||
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
return createAndSubmitClaim(
|
|
||||||
vcClaim as GenericServerRecord,
|
|
||||||
identity,
|
|
||||||
apiServer,
|
|
||||||
axios,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
|
||||||
*
|
|
||||||
* @param identity
|
|
||||||
* @param description may be null; should have this or hours
|
|
||||||
* @param hours may be null; should have this or description
|
|
||||||
* @param expirationDate ISO 8601 date string YYYY-MM-DD (may be null)
|
|
||||||
* @param fulfillsProjectHandleId ID of project to which this contributes (may be null)
|
|
||||||
*/
|
|
||||||
export async function createAndSubmitOffer(
|
|
||||||
axios: Axios,
|
|
||||||
apiServer: string,
|
|
||||||
identity: IIdentifier,
|
|
||||||
description?: string,
|
|
||||||
hours?: number,
|
|
||||||
expirationDate?: string,
|
|
||||||
fulfillsProjectHandleId?: string,
|
|
||||||
): Promise<CreateAndSubmitClaimResult> {
|
|
||||||
const vcClaim: OfferVerifiableCredential = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "Offer",
|
|
||||||
offeredBy: { identifier: identity.did },
|
|
||||||
validThrough: expirationDate || undefined,
|
|
||||||
};
|
|
||||||
if (hours) {
|
|
||||||
vcClaim.includesObject = {
|
|
||||||
amountOfThisGood: hours,
|
|
||||||
unitCode: "HUR",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (description) {
|
|
||||||
vcClaim.itemOffered = { description };
|
|
||||||
}
|
|
||||||
if (fulfillsProjectHandleId) {
|
|
||||||
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
|
||||||
vcClaim.itemOffered.isPartOf = {
|
|
||||||
"@type": "PlanAction",
|
|
||||||
identifier: fulfillsProjectHandleId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return createAndSubmitClaim(
|
|
||||||
vcClaim as GenericServerRecord,
|
|
||||||
identity,
|
|
||||||
apiServer,
|
|
||||||
axios,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createAndSubmitClaim(
|
|
||||||
vcClaim: GenericVerifiableCredential,
|
|
||||||
identity: IIdentifier,
|
|
||||||
apiServer: string,
|
|
||||||
axios: Axios,
|
|
||||||
): Promise<CreateAndSubmitClaimResult> {
|
|
||||||
try {
|
try {
|
||||||
|
const vcClaim: GiveVerifiableCredential = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "GiveAction",
|
||||||
|
recipient: toDid ? { identifier: toDid } : undefined,
|
||||||
|
agent: fromDid ? { identifier: fromDid } : undefined,
|
||||||
|
description: description || undefined,
|
||||||
|
object: hours ? { amountOfThisGood: hours, unitCode: "HUR" } : undefined,
|
||||||
|
fulfills: fulfillsProjectHandleId
|
||||||
|
? { "@type": "PlanAction", identifier: fulfillsProjectHandleId }
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||||
@@ -429,11 +226,15 @@ export async function createAndSubmitClaim(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { type: "success", response };
|
return { type: "success", response };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
} catch (error: unknown) {
|
||||||
} catch (error: any) {
|
|
||||||
console.log("Error creating claim:", error);
|
|
||||||
const errorMessage: string =
|
const errorMessage: string =
|
||||||
error.response?.data?.error?.message || error.message || "Unknown error";
|
error === null
|
||||||
|
? "Null error"
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: typeof error === "object" && error !== null && "message" in error
|
||||||
|
? (error as { message: string }).message
|
||||||
|
: "Unknown error";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -486,10 +287,6 @@ export interface ProjectData {
|
|||||||
* URL referencing information about the project
|
* URL referencing information about the project
|
||||||
**/
|
**/
|
||||||
handleId: string;
|
handleId: string;
|
||||||
/**
|
|
||||||
* The DID of the issuer
|
|
||||||
*/
|
|
||||||
issuerDid: string;
|
|
||||||
/**
|
/**
|
||||||
* The Identier of the project
|
* The Identier of the project
|
||||||
**/
|
**/
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
// many of these are also found in endorser-mobile utility.ts
|
|
||||||
|
|
||||||
import axios, { AxiosResponse } from "axios";
|
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
|
|
||||||
import { db } from "@/db/index";
|
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const Buffer = require("buffer/").Buffer;
|
|
||||||
|
|
||||||
export const isGlobalUri = (uri: string) => {
|
|
||||||
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
|
||||||
};
|
|
||||||
|
|
||||||
// If you edit this, check that the numbers still line up on the side in the alert (on mobile, too),
|
|
||||||
// and make sure they can take all actions while the notification shows.
|
|
||||||
export const ONBOARD_MESSAGE =
|
|
||||||
"1) Check that they've entered their name. 2) Go to the scanning page: use the Contacts page and click on the QR icon at the top, and then scan and register them. 3) Have them go to that page and scan you.";
|
|
||||||
|
|
||||||
export const sendTestThroughPushServer = async (
|
|
||||||
subscription: PushSubscription,
|
|
||||||
skipFilter: boolean,
|
|
||||||
): Promise<AxiosResponse> => {
|
|
||||||
await db.open();
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
|
||||||
if (settings?.webPushServer) {
|
|
||||||
pushUrl = settings.webPushServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
|
|
||||||
// This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
|
|
||||||
// Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213
|
|
||||||
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
|
|
||||||
|
|
||||||
const auth = Buffer.from(subscription.getKey("auth"));
|
|
||||||
const authB64 = auth
|
|
||||||
.toString("base64")
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=+$/, "");
|
|
||||||
const p256dh = Buffer.from(subscription.getKey("p256dh"));
|
|
||||||
const p256dhB64 = p256dh
|
|
||||||
.toString("base64")
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=+$/, "");
|
|
||||||
const newPayload = {
|
|
||||||
endpoint: subscription.endpoint,
|
|
||||||
keys: {
|
|
||||||
auth: authB64,
|
|
||||||
p256dh: p256dhB64,
|
|
||||||
},
|
|
||||||
message: `Test, where you will see this message ${
|
|
||||||
skipFilter ? "un" : ""
|
|
||||||
}filtered.`,
|
|
||||||
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
|
|
||||||
};
|
|
||||||
console.log("Sending a test web push message:", newPayload);
|
|
||||||
const payloadStr = JSON.stringify(newPayload);
|
|
||||||
const response = await axios.post(
|
|
||||||
pushUrl + "/web-push/send-test",
|
|
||||||
payloadStr,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Got response from web push server:", response);
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
14
src/main.ts
@@ -13,9 +13,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
|||||||
import {
|
import {
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faBan,
|
faBan,
|
||||||
faBitcoinSign,
|
|
||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
@@ -29,7 +27,6 @@ import {
|
|||||||
faCoins,
|
faCoins,
|
||||||
faComment,
|
faComment,
|
||||||
faCopy,
|
faCopy,
|
||||||
faDollar,
|
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
@@ -37,26 +34,22 @@ import {
|
|||||||
faFloppyDisk,
|
faFloppyDisk,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faGift,
|
faGift,
|
||||||
faGlobe,
|
|
||||||
faHand,
|
faHand,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
faLocationDot,
|
faLocationDot,
|
||||||
faLongArrowAltLeft,
|
faLongArrowAltLeft,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faMessage,
|
|
||||||
faPen,
|
faPen,
|
||||||
faPersonCircleCheck,
|
faPersonCircleCheck,
|
||||||
faPersonCircleQuestion,
|
faPersonCircleQuestion,
|
||||||
faPlus,
|
faPlus,
|
||||||
faQuestion,
|
|
||||||
faQrcode,
|
faQrcode,
|
||||||
faRotate,
|
faRotate,
|
||||||
faShareNodes,
|
faShareNodes,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
faSquarePlus,
|
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
@@ -67,9 +60,7 @@ import {
|
|||||||
library.add(
|
library.add(
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faBan,
|
faBan,
|
||||||
faBitcoinSign,
|
|
||||||
faBurst,
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
@@ -83,7 +74,6 @@ library.add(
|
|||||||
faCoins,
|
faCoins,
|
||||||
faComment,
|
faComment,
|
||||||
faCopy,
|
faCopy,
|
||||||
faDollar,
|
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
@@ -91,26 +81,22 @@ library.add(
|
|||||||
faFloppyDisk,
|
faFloppyDisk,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faGift,
|
faGift,
|
||||||
faGlobe,
|
|
||||||
faHand,
|
faHand,
|
||||||
faHouseChimney,
|
faHouseChimney,
|
||||||
faLocationDot,
|
faLocationDot,
|
||||||
faLongArrowAltLeft,
|
faLongArrowAltLeft,
|
||||||
faLongArrowAltRight,
|
faLongArrowAltRight,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faMessage,
|
|
||||||
faPen,
|
faPen,
|
||||||
faPersonCircleCheck,
|
faPersonCircleCheck,
|
||||||
faPersonCircleQuestion,
|
faPersonCircleQuestion,
|
||||||
faPlus,
|
faPlus,
|
||||||
faQrcode,
|
faQrcode,
|
||||||
faQuestion,
|
|
||||||
faRotate,
|
faRotate,
|
||||||
faShareNodes,
|
faShareNodes,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
faSquarePlus,
|
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { register } from "register-service-worker";
|
import { register } from "register-service-worker";
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
register("/additional-scripts.js", {
|
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||||
ready() {
|
ready() {
|
||||||
console.log(
|
console.log(
|
||||||
"App is being served from cache by a service worker.\n" +
|
"App is being served from cache by a service worker.\n" +
|
||||||
|
|||||||
@@ -39,12 +39,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "account",
|
name: "account",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
|
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
|
||||||
},
|
beforeEnter: enterOrStart,
|
||||||
{
|
|
||||||
path: "/claim/:id?",
|
|
||||||
name: "claim",
|
|
||||||
component: () =>
|
|
||||||
import(/* webpackChunkName: "claim" */ "../views/ClaimView.vue"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/confirm-contact",
|
path: "/confirm-contact",
|
||||||
@@ -96,14 +91,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
|
import(/* webpackChunkName: "help" */ "../views/HelpView.vue"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/help-notifications",
|
|
||||||
name: "help-notifications",
|
|
||||||
component: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "help-notifications" */ "../views/HelpNotificationsView.vue"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/identity-switcher",
|
path: "/identity-switcher",
|
||||||
name: "identity-switcher",
|
name: "identity-switcher",
|
||||||
@@ -161,7 +148,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/project/:id?",
|
path: "/project",
|
||||||
name: "project",
|
name: "project",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
||||||
@@ -181,14 +168,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/search-area",
|
|
||||||
name: "search-area",
|
|
||||||
component: () =>
|
|
||||||
import(
|
|
||||||
/* webpackChunkName: "search-area" */ "../views/SearchAreaView.vue"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/seed-backup",
|
path: "/seed-backup",
|
||||||
name: "seed-backup",
|
name: "seed-backup",
|
||||||
|
|||||||
2184
src/util.d.ts
vendored
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<TopMessage />
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-4">
|
||||||
Your Identity
|
Your Identity
|
||||||
@@ -34,24 +32,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ID notice -->
|
|
||||||
<div
|
|
||||||
v-if="!activeDid"
|
|
||||||
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>Note:</b> Before you can take any action, you need an ID.
|
|
||||||
</p>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'start' }"
|
|
||||||
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
|
||||||
>
|
|
||||||
Generate Identity
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Registration notice -->
|
<!-- Registration notice -->
|
||||||
<!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
|
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
|
||||||
<div
|
<div
|
||||||
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
|
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"
|
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"
|
||||||
@@ -72,16 +54,13 @@
|
|||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
||||||
{{ givenName }}
|
{{ givenName }}
|
||||||
<router-link :to="{ name: 'new-edit-account' }">
|
|
||||||
<fa icon="pen" class="text-xs text-blue-500 mb-1"></fa>
|
|
||||||
</router-link>
|
|
||||||
</h2>
|
</h2>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'new-edit-account' }"
|
:to="{ name: 'new-edit-account' }"
|
||||||
class="block w-full text-center text-md text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
||||||
>
|
>
|
||||||
(Set Your Name)
|
(set name)
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -100,21 +79,62 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'new-edit-account' }"
|
||||||
|
class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
|
||||||
|
>
|
||||||
|
Edit Identity
|
||||||
|
</router-link>
|
||||||
|
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||||
<div
|
<label
|
||||||
v-if="!notificationMaybeChanged"
|
for="toggleNotifications"
|
||||||
class="flex items-center justify-between cursor-pointer"
|
class="flex items-center cursor-pointer"
|
||||||
@click="showNotificationChoice()"
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'modal',
|
||||||
|
type: 'notification-permission',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<div>App Notifications</div>
|
<div>App Notifications</div>
|
||||||
<!-- toggle -->
|
<!-- toggle -->
|
||||||
|
<div class="relative ml-2">
|
||||||
|
<!-- input -->
|
||||||
|
<input type="checkbox" name="toggleNotifications" class="sr-only" />
|
||||||
|
<!-- line -->
|
||||||
|
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||||
|
<!-- dot -->
|
||||||
|
<div
|
||||||
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
for="toggleMuteNotifications"
|
||||||
|
class="flex items-center cursor-pointer mt-4"
|
||||||
|
@click="
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: 'modal',
|
||||||
|
type: 'notification-mute',
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- label -->
|
||||||
|
<div>Mute Notifications</div>
|
||||||
|
<!-- toggle -->
|
||||||
<div class="relative ml-2">
|
<div class="relative ml-2">
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="isSubscribed"
|
name="toggleMuteNotifications"
|
||||||
name="toggleNotificationsInput"
|
|
||||||
class="sr-only"
|
class="sr-only"
|
||||||
/>
|
/>
|
||||||
<!-- line -->
|
<!-- line -->
|
||||||
@@ -124,79 +144,51 @@
|
|||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</label>
|
||||||
<div v-else>
|
|
||||||
Notification status may have changed. Refresh this page to see the
|
|
||||||
latest setting.
|
|
||||||
</div>
|
|
||||||
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
|
|
||||||
Troubleshoot your notification setup.
|
|
||||||
</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data Export</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'seed-backup' }"
|
:to="{ name: 'seed-backup' }"
|
||||||
v-if="activeDid"
|
href=""
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||||
>
|
>
|
||||||
Backup Identifier Seed
|
Backup Identifier Seed
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<a
|
||||||
<button
|
|
||||||
v-bind:class="computedStartDownloadLinkClassNames()"
|
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
||||||
@click="exportDatabase()"
|
@click="exportDatabase()"
|
||||||
>
|
>
|
||||||
Download Settings & Contacts
|
Download Settings & Contacts
|
||||||
<br />
|
<br />
|
||||||
(excluding Identifier Data)
|
(excluding Identifier Data)
|
||||||
</button>
|
|
||||||
<a
|
|
||||||
ref="downloadLink"
|
|
||||||
v-bind:class="computedDownloadLinkClassNames()"
|
|
||||||
class="block w-full text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
|
|
||||||
>
|
|
||||||
If no download happened yet, click again here to download now.
|
|
||||||
</a>
|
</a>
|
||||||
|
<a ref="downloadLink" />
|
||||||
|
|
||||||
<div v-if="activeDid" class="flex mt-8 py-2">
|
<div v-if="activeDid" class="flex py-2">
|
||||||
<h3 class="text-sm uppercase font-semibold">Rate Limits</h3>
|
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
||||||
<button
|
|
||||||
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md ml-2 mr-2 mb-2"
|
|
||||||
@click="checkLimits()"
|
|
||||||
>
|
|
||||||
Check Limits
|
Check Limits
|
||||||
</button>
|
</button>
|
||||||
<!-- show spinner if loading limits -->
|
<!-- show spinner if loading limits -->
|
||||||
<div v-if="loadingLimits" class="text-center">
|
<div v-if="loadingLimits" class="ml-2">
|
||||||
Checking… <fa icon="spinner" class="fa-spin"></fa>
|
Checking... <fa icon="spinner" class="fa-spin"></fa>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="ml-2">
|
||||||
{{ limitsMessage }}
|
{{ limitsMessage }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!!limits?.nextWeekBeginDateTime">
|
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
||||||
<p class="mb-3 text-sm">
|
<span class="font-bold">Rate Limits</span>
|
||||||
You have done <b>{{ limits.doneClaimsThisWeek }}</b> claims out of
|
<p>
|
||||||
<b>{{ limits.maxClaimsPerWeek }}</b> for this week. Your claims
|
You have done {{ limits.doneClaimsThisWeek }} claims out of
|
||||||
counter resets at
|
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
|
||||||
<b class="whitespace-nowrap">{{
|
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
|
||||||
readableTime(limits.nextWeekBeginDateTime)
|
|
||||||
}}</b>
|
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm">
|
<p>
|
||||||
You have done
|
You have done {{ limits.doneRegistrationsThisMonth }} registrations
|
||||||
<b>{{ limits.doneRegistrationsThisMonth }}</b> registrations out of
|
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
|
||||||
<b>{{ limits.maxRegistrationsPerMonth }}</b> for this month.
|
registrations counter resets at
|
||||||
<i
|
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
||||||
>(You can register nobody on your first day, and after that only one
|
|
||||||
a day in your first month.)</i
|
|
||||||
>
|
|
||||||
Your registration counter resets at
|
|
||||||
<b class="whitespace-nowrap">
|
|
||||||
{{ readableTime(limits.nextMonthBeginDateTime) }}
|
|
||||||
</b>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,14 +201,10 @@
|
|||||||
>
|
>
|
||||||
Advanced
|
Advanced
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="showAdvanced">
|
|
||||||
<p class="text-rose-600 mb-8">
|
|
||||||
Beware: the features here can be confusing and even change data in ways
|
|
||||||
you do not expect. But we support your freedom!
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
<div v-if="showAdvanced">
|
||||||
<!-- Deep Identity Details -->
|
<!-- Deep Identity Details -->
|
||||||
<h2 class="text-sm uppercase font-semibold mb-3">
|
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
|
||||||
Deep Identity Details
|
Deep Identity Details
|
||||||
</h2>
|
</h2>
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
@@ -274,11 +262,13 @@
|
|||||||
|
|
||||||
<label
|
<label
|
||||||
for="toggleShowAmounts"
|
for="toggleShowAmounts"
|
||||||
class="flex items-center justify-between cursor-pointer my-4"
|
class="flex items-center cursor-pointer py-2"
|
||||||
@click="handleChange"
|
@click="handleChange"
|
||||||
>
|
>
|
||||||
<!-- label -->
|
<!-- label -->
|
||||||
<h2>Show amounts given with contacts</h2>
|
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||||
|
Show amounts given with contacts
|
||||||
|
</h2>
|
||||||
<!-- toggle -->
|
<!-- toggle -->
|
||||||
<div class="relative ml-2">
|
<div class="relative ml-2">
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
@@ -297,40 +287,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="grid-cols-2 mb-4">
|
|
||||||
<span class="text-slate-500 text-sm font-bold mb-2">Data Import</span>
|
|
||||||
<input type="file" @change="uploadFile" class="ml-2" />
|
|
||||||
<div v-if="showContactImport()">
|
|
||||||
<button
|
|
||||||
class="block text-center text-md uppercase bg-green-600 text-white px-1.5 py-2 rounded-md mb-6"
|
|
||||||
@click="submitFile()"
|
|
||||||
>
|
|
||||||
Import Settings & Contacts
|
|
||||||
<br />
|
|
||||||
(excluding Identifier Data)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
<button>
|
<button class="text-blue-500">
|
||||||
|
<!-- id used by puppeteer test script -->
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'statistics' }"
|
id="switch-identity-link"
|
||||||
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
:to="{ name: 'identity-switcher' }"
|
||||||
|
class="block text-center"
|
||||||
>
|
>
|
||||||
See Global Animated History of Giving
|
Switch Identity / No Identity
|
||||||
</router-link>
|
</router-link>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
<div class="flex py-2">
|
||||||
<router-link
|
<button class="text-blue-500">
|
||||||
id="switch-identity-link"
|
<router-link :to="{ name: 'statistics' }" class="block text-center">
|
||||||
:to="{ name: 'identity-switcher' }"
|
See Achievements & Statistics
|
||||||
class="block w-fit text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
</router-link>
|
||||||
>
|
</button>
|
||||||
Switch Identity
|
</div>
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<div class="flex py-4">
|
<div class="flex py-4">
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
||||||
@@ -347,126 +323,41 @@
|
|||||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||||
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
@click="setApiServerInput(Constants.PROD_ENDORSER_API_SERVER)"
|
||||||
>
|
>
|
||||||
Use Prod
|
Use Prod
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||||
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
@click="setApiServerInput(Constants.TEST_ENDORSER_API_SERVER)"
|
||||||
>
|
>
|
||||||
Use Test
|
Use Test
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||||
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
@click="setApiServerInput(Constants.LOCAL_ENDORSER_API_SERVER)"
|
||||||
>
|
>
|
||||||
Use Local
|
Use Local
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label
|
|
||||||
for="toggleProdWarningMessage"
|
|
||||||
class="flex items-center justify-between cursor-pointer my-4"
|
|
||||||
@click="toggleProdWarning"
|
|
||||||
>
|
|
||||||
<!-- label -->
|
|
||||||
<h2>Show warning if on prod server</h2>
|
|
||||||
<!-- toggle -->
|
|
||||||
<div class="relative ml-2">
|
|
||||||
<!-- input -->
|
|
||||||
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
|
|
||||||
<!-- line -->
|
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
|
||||||
<!-- dot -->
|
|
||||||
<div
|
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
for="toggleTestWarningMessage"
|
|
||||||
class="flex items-center justify-between cursor-pointer my-4"
|
|
||||||
@click="toggleTestWarning"
|
|
||||||
>
|
|
||||||
<!-- label -->
|
|
||||||
<h2>Show warning if on non-prod server</h2>
|
|
||||||
<!-- toggle -->
|
|
||||||
<div class="relative ml-2">
|
|
||||||
<!-- input -->
|
|
||||||
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
|
|
||||||
<!-- line -->
|
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
|
||||||
<!-- dot -->
|
|
||||||
<div
|
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="flex py-4">
|
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
|
||||||
Notification Push Server
|
|
||||||
</h2>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
|
||||||
v-model="webPushServerInput"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-if="webPushServerInput != webPushServer"
|
|
||||||
class="px-4 rounded bg-red-500 border border-slate-400"
|
|
||||||
@click="onClickSavePushServer()"
|
|
||||||
>
|
|
||||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
||||||
@click="webPushServerInput = AppConstants.PROD_PUSH_SERVER"
|
|
||||||
>
|
|
||||||
Use Prod
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
||||||
@click="webPushServerInput = AppConstants.TEST1_PUSH_SERVER"
|
|
||||||
>
|
|
||||||
Use Test 1
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
|
||||||
@click="webPushServerInput = AppConstants.TEST2_PUSH_SERVER"
|
|
||||||
>
|
|
||||||
Use Test 2
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span class="px-4 text-sm" v-if="!webPushServerInput">
|
|
||||||
When that setting is blank, this app will use the default web push
|
|
||||||
server URL:
|
|
||||||
{{ AppConstants.DEFAULT_PUSH_SERVER }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError, AxiosRequestConfig } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import Dexie from "dexie";
|
|
||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
import { ref } from "vue";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
|
||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
||||||
import { ImportProgress } from "dexie-export-import/dist/import";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
@@ -485,28 +376,21 @@ interface IAccount {
|
|||||||
derivationPath: string;
|
derivationPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputFileNameRef = ref<Blob>();
|
@Component({ components: { QuickNav } })
|
||||||
|
|
||||||
@Component({ components: { QuickNav, TopMessage } })
|
|
||||||
export default class AccountViewView extends Vue {
|
export default class AccountViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
AppConstants = AppString;
|
Constants = AppString;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
apiServerInput = "";
|
apiServerInput = "";
|
||||||
derivationPath = "";
|
derivationPath = "";
|
||||||
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
|
|
||||||
givenName = "";
|
givenName = "";
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
isSubscribed = false;
|
|
||||||
notificationMaybeChanged = false;
|
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
publicHex = "";
|
publicHex = "";
|
||||||
publicBase64 = "";
|
publicBase64 = "";
|
||||||
webPushServer = "";
|
|
||||||
webPushServerInput = "";
|
|
||||||
limits: RateLimits | null = null;
|
limits: RateLimits | null = null;
|
||||||
limitsMessage = "";
|
limitsMessage = "";
|
||||||
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
|
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
|
||||||
@@ -519,91 +403,30 @@ export default class AccountViewView extends Vue {
|
|||||||
|
|
||||||
showAdvanced = false;
|
showAdvanced = false;
|
||||||
|
|
||||||
subscription: PushSubscription | null = null;
|
|
||||||
warnIfProdServer = false;
|
|
||||||
warnIfTestServer = false;
|
|
||||||
|
|
||||||
async beforeCreate() {
|
|
||||||
await accountsDB.open();
|
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Async function executed when the component is created.
|
|
||||||
* Initializes the component's state with values from the database,
|
|
||||||
* handles identity-related tasks, and checks limitations.
|
|
||||||
*
|
|
||||||
* @throws Will display specific messages to the user based on different errors.
|
|
||||||
*/
|
|
||||||
async created() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
|
|
||||||
// Initialize component state with values from the database or defaults
|
|
||||||
this.initializeState(settings);
|
|
||||||
|
|
||||||
// Get and process the identity
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
|
||||||
if (identity) {
|
|
||||||
this.processIdentity(identity);
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
this.handleError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
try {
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
|
||||||
this.subscription = await registration.pushManager.getSubscription();
|
|
||||||
this.isSubscribed = !!this.subscription;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Mount error:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeUnmount() {
|
|
||||||
if (this.downloadUrl) {
|
|
||||||
URL.revokeObjectURL(this.downloadUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes component state with values from the database or defaults.
|
|
||||||
* @param {SettingsType} settings - Object containing settings from the database.
|
|
||||||
*/
|
|
||||||
initializeState(settings: Settings | undefined) {
|
|
||||||
this.activeDid = (settings?.activeDid as string) || "";
|
|
||||||
this.apiServer = (settings?.apiServer as string) || "";
|
|
||||||
this.apiServerInput = (settings?.apiServer as string) || "";
|
|
||||||
this.givenName =
|
|
||||||
(settings?.firstName || "") +
|
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
|
||||||
this.warnIfProdServer = !!settings?.warnIfProdServer;
|
|
||||||
this.warnIfTestServer = !!settings?.warnIfTestServer;
|
|
||||||
this.webPushServer = (settings?.webPushServer as string) || "";
|
|
||||||
this.webPushServerInput = (settings?.webPushServer as string) || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
||||||
try {
|
try {
|
||||||
// Open the accounts database
|
// Open the accounts database
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open accounts database:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let account: { identity?: string } | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
// Search for the account with the matching DID (decentralized identifier)
|
// Search for the account with the matching DID (decentralized identifier)
|
||||||
const account: { identity?: string } | undefined =
|
account = await accountsDB.accounts
|
||||||
await accountsDB.accounts.where("did").equals(activeDid).first();
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
// Return parsed identity or null if not found
|
.first();
|
||||||
return JSON.parse((account?.identity as string) || "null");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to find account:", error);
|
console.error("Failed to find account:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return parsed identity or null if not found
|
||||||
|
return JSON.parse((account?.identity as string) || "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -645,20 +468,56 @@ export default class AccountViewView extends Vue {
|
|||||||
this.updateShowContactAmounts();
|
this.updateShowContactAmounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleProdWarning() {
|
|
||||||
this.warnIfProdServer = !this.warnIfProdServer;
|
|
||||||
this.updateWarnIfProdServer(this.warnIfProdServer);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTestWarning() {
|
|
||||||
this.warnIfTestServer = !this.warnIfTestServer;
|
|
||||||
this.updateWarnIfTestServer(this.warnIfTestServer);
|
|
||||||
}
|
|
||||||
|
|
||||||
readableTime(timeStr: string) {
|
readableTime(timeStr: string) {
|
||||||
return timeStr.substring(0, timeStr.indexOf("T"));
|
return timeStr.substring(0, timeStr.indexOf("T"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async beforeCreate() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async function executed when the component is created.
|
||||||
|
* Initializes the component's state with values from the database,
|
||||||
|
* handles identity-related tasks, and checks limitations.
|
||||||
|
*
|
||||||
|
* @throws Will display specific messages to the user based on different errors.
|
||||||
|
*/
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
|
||||||
|
// Initialize component state with values from the database or defaults
|
||||||
|
this.initializeState(settings);
|
||||||
|
|
||||||
|
// Get and process the identity
|
||||||
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
if (identity) {
|
||||||
|
this.processIdentity(identity);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
this.handleError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes component state with values from the database or defaults.
|
||||||
|
* @param {SettingsType} settings - Object containing settings from the database.
|
||||||
|
*/
|
||||||
|
initializeState(settings: Settings | undefined) {
|
||||||
|
this.activeDid = (settings?.activeDid as string) || "";
|
||||||
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
|
this.apiServerInput = (settings?.apiServer as string) || "";
|
||||||
|
this.givenName =
|
||||||
|
(settings?.firstName || "") +
|
||||||
|
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
||||||
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the identity and updates the component's state.
|
* Processes the identity and updates the component's state.
|
||||||
* @param {IdentityType} identity - Object containing identity information.
|
* @param {IdentityType} identity - Object containing identity information.
|
||||||
@@ -683,31 +542,6 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async showNotificationChoice() {
|
|
||||||
if (!this.subscription) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "modal",
|
|
||||||
type: "notification-permission",
|
|
||||||
title: "", // unused, only here to satisfy type check
|
|
||||||
text: "", // unused, only here to satisfy type check
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "modal",
|
|
||||||
type: "notification-off",
|
|
||||||
title: "", // unused, only here to satisfy type check
|
|
||||||
text: "", // unused, only here to satisfy type check
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.notificationMaybeChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles errors and updates the component's state accordingly.
|
* Handles errors and updates the component's state accordingly.
|
||||||
* @param {Error} err - The error object.
|
* @param {Error} err - The error object.
|
||||||
@@ -746,58 +580,12 @@ export default class AccountViewView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Updating Contact Setting",
|
title: "Error Updating Contact Setting",
|
||||||
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
text: "Clear your cache and start over (after data backup).",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Telling user to try again after contact setting update because:",
|
"Telling user to clear cache after contact setting update because:",
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateWarnIfProdServer(newSetting: boolean) {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
warnIfProdServer: newSetting,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Updating Prod Warning",
|
|
||||||
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Telling user to try again after setting update because:",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateWarnIfTestServer(newSetting: boolean) {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
warnIfTestServer: newSetting,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Updating Test Warning",
|
|
||||||
text: "The setting may not have saved. Try again, maybe after restarting the app.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Telling user to try again after setting update because:",
|
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -814,13 +602,13 @@ export default class AccountViewView extends Vue {
|
|||||||
const blob = await this.generateDatabaseBlob();
|
const blob = await this.generateDatabaseBlob();
|
||||||
|
|
||||||
// Create a temporary URL for the blob
|
// Create a temporary URL for the blob
|
||||||
this.downloadUrl = this.createBlobURL(blob);
|
const url = this.createBlobURL(blob);
|
||||||
|
|
||||||
// Trigger the download
|
// Trigger the download
|
||||||
this.downloadDatabaseBackup(this.downloadUrl);
|
this.downloadDatabaseBackup(url);
|
||||||
|
|
||||||
// Revoke the temporary URL -- not yet because of DuckDuckGo download failure
|
// Revoke the temporary URL
|
||||||
//URL.revokeObjectURL(this.downloadUrl);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
// Notify the user that the download has started
|
// Notify the user that the download has started
|
||||||
this.notifyDownloadStarted();
|
this.notifyDownloadStarted();
|
||||||
@@ -857,19 +645,7 @@ export default class AccountViewView extends Vue {
|
|||||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||||
downloadAnchor.href = url;
|
downloadAnchor.href = url;
|
||||||
downloadAnchor.download = `${db.name}-backup.json`;
|
downloadAnchor.download = `${db.name}-backup.json`;
|
||||||
downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo
|
downloadAnchor.click();
|
||||||
}
|
|
||||||
|
|
||||||
public computedStartDownloadLinkClassNames() {
|
|
||||||
return {
|
|
||||||
invisible: this.downloadUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public computedDownloadLinkClassNames() {
|
|
||||||
return {
|
|
||||||
invisible: !this.downloadUrl,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -879,11 +655,11 @@ export default class AccountViewView extends Vue {
|
|||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "success",
|
type: "toast",
|
||||||
title: "Download Started",
|
title: "Download Started",
|
||||||
text: "See your downloads directory for the backup. It is in the Dexie format.",
|
text: "See your downloads directory for the backup.",
|
||||||
},
|
},
|
||||||
-1,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,43 +681,6 @@ export default class AccountViewView extends Vue {
|
|||||||
console.error("Export Error:", error);
|
console.error("Export Error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
async uploadFile(event: any) {
|
|
||||||
inputFileNameRef.value = event.target.files[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
showContactImport() {
|
|
||||||
return !!inputFileNameRef.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously imports the database from a downloadable JSON file.
|
|
||||||
*
|
|
||||||
* @throws Will notify the user if there is an export error.
|
|
||||||
*/
|
|
||||||
async submitFile() {
|
|
||||||
if (inputFileNameRef.value != null) {
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"This will replace all settings and contacts, so we recommend you first do the backup step above." +
|
|
||||||
" Are you sure you want to import and replace all contacts and settings?",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await db.delete();
|
|
||||||
await Dexie.import(inputFileNameRef.value, {
|
|
||||||
progressCallback: this.progressCallback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private progressCallback(progress: ImportProgress) {
|
|
||||||
console.log(
|
|
||||||
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkLimits() {
|
async checkLimits() {
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
if (identity) {
|
if (identity) {
|
||||||
@@ -971,7 +710,7 @@ export default class AccountViewView extends Vue {
|
|||||||
});
|
});
|
||||||
this.isRegistered = true;
|
this.isRegistered = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Got an error updating settings:", err);
|
console.log("Got an error updating settings:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -1000,7 +739,7 @@ export default class AccountViewView extends Vue {
|
|||||||
private async fetchRateLimits(identity: IIdentifier) {
|
private async fetchRateLimits(identity: IIdentifier) {
|
||||||
const url = `${this.apiServer}/api/report/rateLimits`;
|
const url = `${this.apiServer}/api/report/rateLimits`;
|
||||||
const headers = await this.getHeaders(identity);
|
const headers = await this.getHeaders(identity);
|
||||||
return await this.axios.get(url, { headers } as AxiosRequestConfig);
|
return await this.axios.get(url, { headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1013,9 +752,10 @@ export default class AccountViewView extends Vue {
|
|||||||
const data = error.response?.data as ErrorResponse;
|
const data = error.response?.data as ErrorResponse;
|
||||||
this.limitsMessage =
|
this.limitsMessage =
|
||||||
(data?.error?.message as string) || "Bad server response.";
|
(data?.error?.message as string) || "Bad server response.";
|
||||||
console.error(
|
console.log(
|
||||||
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
|
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
|
||||||
this.limitsMessage,
|
this.limitsMessage,
|
||||||
|
//error,
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
@@ -1071,7 +811,6 @@ export default class AccountViewView extends Vue {
|
|||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = accounts[accountNum - 1];
|
const account = accounts[accountNum - 1];
|
||||||
|
|
||||||
await db.open();
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
|
await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did });
|
||||||
|
|
||||||
this.updateActiveAccountProperties(account);
|
this.updateActiveAccountProperties(account);
|
||||||
@@ -1104,21 +843,8 @@ export default class AccountViewView extends Vue {
|
|||||||
this.apiServer = this.apiServerInput;
|
this.apiServer = this.apiServerInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickSavePushServer() {
|
setApiServerInput(value: string) {
|
||||||
await db.open();
|
this.apiServerInput = value;
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
webPushServer: this.webPushServerInput,
|
|
||||||
});
|
|
||||||
this.webPushServer = this.webPushServerInput;
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "Reload",
|
|
||||||
text: "Now reload the app to get a new VAPID to use with this push server.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,483 +0,0 @@
|
|||||||
<template>
|
|
||||||
<QuickNav />
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
||||||
<!-- Breadcrumb -->
|
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
|
||||||
<!-- Back -->
|
|
||||||
<button
|
|
||||||
@click="$router.go(-1)"
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</button>
|
|
||||||
Verifiable Claim Details
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Details -->
|
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
|
||||||
<div>
|
|
||||||
<div class="block flex gap-4 overflow-hidden">
|
|
||||||
<div class="overflow-hidden">
|
|
||||||
<h2 class="text-md font-bold">{{ veriClaim.id }}</h2>
|
|
||||||
<div class="text-sm">
|
|
||||||
<div>
|
|
||||||
{{ veriClaim.claimType }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<fa icon="message" class="fa-fw text-slate-400"></fa>
|
|
||||||
{{ veriClaim.claim?.description }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
|
||||||
{{ veriClaim.issuer }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
|
||||||
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Confirmations</h2>
|
|
||||||
|
|
||||||
<span v-if="totalConfirmers() === 0">Nobody has confirmed this.</span>
|
|
||||||
<span v-else-if="totalConfirmers() === 1">
|
|
||||||
One person has confirmed this.
|
|
||||||
</span>
|
|
||||||
<span v-else> {{ totalConfirmers() }} people have confirmed this. </span>
|
|
||||||
|
|
||||||
<div v-if="totalConfirmers() > 0">
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
confirmerIdList.length === 0 && confsVisibleToIdList.length === 0
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Nobody that you know confirmed this claim, nor do they have any
|
|
||||||
confirmers in their network.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="confirmerIdList.length === 0 && confsVisibleToIdList.length > 0"
|
|
||||||
>
|
|
||||||
<!-- Only show if this person has links to confirmers (below). -->
|
|
||||||
Nobody that you know has issued or confirmed this claim.
|
|
||||||
</div>
|
|
||||||
<div v-if="confirmerIdList.length > 0">
|
|
||||||
The following people have issued or confirmed this claim.
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="confirmerId in confirmerIdList"
|
|
||||||
:key="confirmerId"
|
|
||||||
class="list-disc"
|
|
||||||
>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<div class="grow overflow-hidden">
|
|
||||||
<div class="text-sm">
|
|
||||||
{{ confirmerId }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Never need to show the following message.
|
|
||||||
If there is nobody in the confirmerIdList then we'll show the combined "nobody" message.
|
|
||||||
If there is somebody in the confirmerIdList then that's all they need to show.
|
|
||||||
-->
|
|
||||||
<!-- Nobody that you know can see someone who has confirmed this claim. -->
|
|
||||||
|
|
||||||
<div v-if="confsVisibleToIdList.length > 0">
|
|
||||||
The following people can connect you with people who have issued or
|
|
||||||
confirmed this claim.
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="confsVisibleTo in confsVisibleToIdList"
|
|
||||||
:key="confsVisibleTo"
|
|
||||||
class="list-disc"
|
|
||||||
>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<div class="grow overflow-hidden">
|
|
||||||
<div class="text-sm">
|
|
||||||
{{ confsVisibleTo }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
<div v-if="confirmerIdList.includes(activeDid)">
|
|
||||||
You have confirmed this claim.
|
|
||||||
</div>
|
|
||||||
<div v-else-if="containsHiddenDid(veriClaim.claim)">
|
|
||||||
You cannot confirm this claim because it contains data that is hidden
|
|
||||||
from you.
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<button
|
|
||||||
class="bg-blue-600 text-white mt-4 px-4 py-2 rounded-md mb-4"
|
|
||||||
@click="confirmClaim(veriClaim.id)"
|
|
||||||
>
|
|
||||||
Confirm Claim
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Claim</h2>
|
|
||||||
<pre
|
|
||||||
class="text-sm overflow-x-scroll px-4 py-3 bg-slate-100 rounded-md"
|
|
||||||
>{{ veriClaimDump }}</pre
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="font-bold uppercase text-xl mt-8 mb-2">Full Claim</h2>
|
|
||||||
<p class="mb-4">
|
|
||||||
The full claim includes the claim as it was originally issued, including
|
|
||||||
the signature (ie. the proof of issuance by that person).
|
|
||||||
</p>
|
|
||||||
<div v-if="!fullClaim">
|
|
||||||
<p v-if="fullClaimMessage" class="mb-4">
|
|
||||||
{{ fullClaimMessage }}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
|
|
||||||
@click="showFullClaim(veriClaim.id)"
|
|
||||||
>
|
|
||||||
Load Full Claim Details
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<pre>{{ fullClaimDump }}</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
:href="apiServer + '/api/claim/' + veriClaim.id"
|
|
||||||
target="_blank"
|
|
||||||
class="block w-full text-center text-md uppercase bg-blue-600 text-white px-1.5 py-2 rounded-md mb-2"
|
|
||||||
>
|
|
||||||
View on the Public Server
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
|
||||||
import * as yaml from "js-yaml";
|
|
||||||
import * as R from "ramda";
|
|
||||||
import { IIdentifier } from "@veramo/core";
|
|
||||||
import * as util from "util";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
|
||||||
import OfferDialog from "@/components/OfferDialog.vue";
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
|
||||||
import { Contact } from "@/db/tables/contacts";
|
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
|
||||||
import { accessToken } from "@/libs/crypto";
|
|
||||||
import * as serverUtil from "@/libs/endorserServer";
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
group: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav },
|
|
||||||
})
|
|
||||||
export default class ClaimView extends Vue {
|
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
activeDid = "";
|
|
||||||
allMyDids: Array<string> = [];
|
|
||||||
allContacts: Array<Contact> = [];
|
|
||||||
apiServer = "";
|
|
||||||
confirmerIdList = []; // list of DIDs that have confirmed this claim excluding the issuer
|
|
||||||
confsVisibleErrorMessage = "";
|
|
||||||
confsVisibleToIdList = []; // list of DIDs that can see any confirmer
|
|
||||||
fullClaim = null;
|
|
||||||
fullClaimDump = "";
|
|
||||||
fullClaimMessage = "";
|
|
||||||
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
|
|
||||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
|
||||||
veriClaimDump = "";
|
|
||||||
|
|
||||||
util = util;
|
|
||||||
yaml = yaml;
|
|
||||||
containsHiddenDid = serverUtil.containsHiddenDid;
|
|
||||||
|
|
||||||
async created() {
|
|
||||||
await db.open();
|
|
||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
|
||||||
this.activeDid = settings?.activeDid || "";
|
|
||||||
this.apiServer = settings?.apiServer || "";
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
|
||||||
|
|
||||||
await accountsDB.open();
|
|
||||||
const accounts = accountsDB.accounts;
|
|
||||||
const accountsArr = await accounts?.toArray();
|
|
||||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
|
|
||||||
const pathParam = window.location.pathname.substring("/claim/".length);
|
|
||||||
let claimId;
|
|
||||||
if (pathParam) {
|
|
||||||
claimId = decodeURIComponent(pathParam);
|
|
||||||
this.loadClaim(claimId, identity);
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "No claim ID was provided.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalConfirmers() {
|
|
||||||
return (
|
|
||||||
this.numConfsNotVisible +
|
|
||||||
this.confirmerIdList.length +
|
|
||||||
this.confsVisibleToIdList.length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getIdentity(activeDid: string): Promise<IIdentifier> {
|
|
||||||
await accountsDB.open();
|
|
||||||
const account = (await accountsDB.accounts
|
|
||||||
.where("did")
|
|
||||||
.equals(activeDid)
|
|
||||||
.first()) as Account;
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error(
|
|
||||||
"Attempted to load project records with no identity available.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getHeaders(identity: IIdentifier) {
|
|
||||||
const headers: RawAxiosRequestHeaders = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
};
|
|
||||||
if (identity) {
|
|
||||||
const token = await accessToken(identity);
|
|
||||||
headers["Authorization"] = "Bearer " + token;
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Isn't there a better way to make this available to the template?
|
|
||||||
didInfo(
|
|
||||||
did: string,
|
|
||||||
activeDid: string,
|
|
||||||
dids: Array<string>,
|
|
||||||
contacts: Array<Contact>,
|
|
||||||
) {
|
|
||||||
return serverUtil.didInfo(did, activeDid, dids, contacts);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadClaim(claimId: string, identity: IIdentifier) {
|
|
||||||
const url =
|
|
||||||
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(claimId);
|
|
||||||
const headers = await this.getHeaders(identity);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await this.axios.get(url, { headers });
|
|
||||||
if (resp.status === 200) {
|
|
||||||
this.veriClaim = resp.data;
|
|
||||||
this.veriClaimDump = yaml.dump(this.veriClaim);
|
|
||||||
} else {
|
|
||||||
// actually, axios typically throws an error so we never get here
|
|
||||||
console.log("Error getting claim:", resp);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "There was a problem getting that claim. See logs for more info.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const serverError = error as AxiosError;
|
|
||||||
console.error("Error retrieving claim:", serverError);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Something went wrong retrieving that claim. See logs for more info.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmUrl =
|
|
||||||
this.apiServer +
|
|
||||||
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
|
|
||||||
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
|
|
||||||
const confirmHeaders = await this.getHeaders(identity);
|
|
||||||
try {
|
|
||||||
const response = await this.axios.get(confirmUrl, {
|
|
||||||
headers: confirmHeaders,
|
|
||||||
});
|
|
||||||
if (response.status === 200) {
|
|
||||||
const resultList1 = response.data.result || [];
|
|
||||||
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
|
|
||||||
const resultList3 = R.reject(
|
|
||||||
(did: string) => did === this.veriClaim.issuer,
|
|
||||||
resultList2,
|
|
||||||
);
|
|
||||||
this.confirmerIdList = resultList3;
|
|
||||||
this.numConfsNotVisible = resultList1.length - resultList2.length;
|
|
||||||
if (resultList3.length === resultList2.length) {
|
|
||||||
// the issuer was not in the "visible" list so they must be hidden
|
|
||||||
// so subtract them from the non-visible confirmers count
|
|
||||||
this.numConfsNotVisible = this.numConfsNotVisible - 1;
|
|
||||||
}
|
|
||||||
this.confsVisibleToIdList =
|
|
||||||
response.data.result.resultVisibleToDids || [];
|
|
||||||
} else {
|
|
||||||
this.confsVisibleErrorMessage =
|
|
||||||
"Had problems retrieving confirmations.";
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const serverError = error as AxiosError;
|
|
||||||
console.error("Error retrieving confirmations:", serverError);
|
|
||||||
this.confsVisibleErrorMessage =
|
|
||||||
"Had problems retrieving confirmations. See logs for more info.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async showFullClaim(claimId: string) {
|
|
||||||
await accountsDB.open();
|
|
||||||
const accounts = accountsDB.accounts;
|
|
||||||
const accountsArr = await accounts?.toArray();
|
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
|
|
||||||
const url =
|
|
||||||
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
|
|
||||||
const headers = await this.getHeaders(identity);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await this.axios.get(url, { headers });
|
|
||||||
if (resp.status === 200) {
|
|
||||||
this.fullClaim = resp.data;
|
|
||||||
this.fullClaimDump = yaml.dump(this.fullClaim);
|
|
||||||
} else {
|
|
||||||
// actually, axios typically throws an error so we never get here
|
|
||||||
console.log("Error getting full claim:", resp);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "There was a problem getting that claim. See logs for more info.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error("Error retrieving full claim:", error);
|
|
||||||
const serverError = error as AxiosError;
|
|
||||||
if (serverError.response?.status === 403) {
|
|
||||||
this.fullClaimMessage =
|
|
||||||
"You are not authorized to view the full contents of this claim." +
|
|
||||||
" To see all the details, ask the issuer to allow you to see their claims." +
|
|
||||||
" If you cannot see the issuer's DID, ask someone in the Confirmations section above." +
|
|
||||||
" If there are no connections, you will have to ask people in your" +
|
|
||||||
" network for their help, some other way; send them to this page and" +
|
|
||||||
" see if they can make a connection for you.";
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Something went wrong retrieving that claim. See logs for more info.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async confirmClaim() {
|
|
||||||
if (confirm("Do you personally confirm that this is true?")) {
|
|
||||||
// similar logic is found in endorser-mobile
|
|
||||||
const goodClaim = serverUtil.removeSchemaContext(
|
|
||||||
serverUtil.removeVisibleToDids(
|
|
||||||
serverUtil.addLastClaimOrHandleAsIdIfMissing(
|
|
||||||
this.veriClaim.claim,
|
|
||||||
this.veriClaim.id,
|
|
||||||
this.veriClaim.handleId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const confirmationClaim: serverUtil.GenericVerifiableCredential & {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
object: any;
|
|
||||||
} = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "AgreeAction",
|
|
||||||
object: goodClaim,
|
|
||||||
};
|
|
||||||
const result = await serverUtil.createAndSubmitClaim(
|
|
||||||
confirmationClaim,
|
|
||||||
await this.getIdentity(this.activeDid),
|
|
||||||
this.apiServer,
|
|
||||||
this.axios,
|
|
||||||
);
|
|
||||||
if (result.type === "success") {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Success",
|
|
||||||
text: "Confirmation submitted.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log("Got error submitting the confirmation:", result);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "There was a problem submitting the confirmation. See logs for more info.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Contacts"></QuickNav>
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
id="ViewBreadcrumb"
|
|
||||||
class="text-lg text-center font-light relative px-7"
|
|
||||||
>
|
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contacts' }"
|
:to="{ name: 'contacts' }"
|
||||||
@@ -14,24 +11,16 @@
|
|||||||
><fa icon="chevron-left" class="fa-fw"></fa
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
></router-link>
|
></router-link>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
|
||||||
Given with {{ contact?.name }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Given with {{ contact?.name }}
|
||||||
|
</h1>
|
||||||
<div class="flex justify-around">
|
<div class="flex justify-around">
|
||||||
<span />
|
<span />
|
||||||
<span class="justify-around">(Only 50 most recent)</span>
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
<span />
|
<span />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-around">
|
|
||||||
<span />
|
|
||||||
<span class="justify-around">
|
|
||||||
(This does not include claims by them if they're not visible to you.)
|
|
||||||
</span>
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<table
|
<table
|
||||||
@@ -131,7 +120,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
export default class ContactAmountssView extends Vue {
|
export default class ContactsView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
@@ -185,7 +174,6 @@ export default class ContactAmountssView extends Vue {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log("Error retrieving settings or gives.", err);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -193,7 +181,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.userMessage ||
|
err.userMessage ||
|
||||||
"There was an error retrieving your settings and/or contacts and/or gives.",
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -370,10 +358,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/*
|
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
||||||
Tooltip, generated on "title" attributes on "fa" icons
|
|
||||||
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
|
||||||
*/
|
|
||||||
/* Tooltip container */
|
/* Tooltip container */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Home"></QuickNav>
|
<QuickNav selected="Home"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -66,11 +66,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<GiftedDialog
|
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
|
||||||
ref="customDialog"
|
|
||||||
message="Received from"
|
|
||||||
showGivenToUser="true"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -145,7 +141,6 @@ export default class ContactGiftingView extends Vue {
|
|||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log("Error retrieving settings & contacts:", err);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -153,7 +148,7 @@ export default class ContactGiftingView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.message ||
|
err.message ||
|
||||||
"There was an error retrieving your settings and/or contacts.",
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,82 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Heading -->
|
||||||
<div class="mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||||
<!-- Back -->
|
Your Contact Info
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
</h1>
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
<!--
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
Play with display options: https://qr-code-styling.com/
|
||||||
Your Contact Info
|
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
|
||||||
</h1>
|
-->
|
||||||
<p v-if="!givenName" class="text-center mt-2">
|
<QRCodeVue3
|
||||||
<span class="text-red">Beware!</span>
|
:value="this.qrValue"
|
||||||
You aren't sharing your name, so hurry and
|
:cornersSquareOptions="{ type: 'extra-rounded' }"
|
||||||
<router-link
|
:dotsOptions="{ type: 'square' }"
|
||||||
:to="{ name: 'new-edit-account' }"
|
class="flex justify-center"
|
||||||
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
/>
|
||||||
>
|
|
||||||
go here to set it for them.
|
|
||||||
</router-link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div @click="onCopyToClipboard()" v-if="activeDid">
|
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
|
||||||
<!--
|
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="text-center" v-else>
|
|
||||||
You have no identitifiers yet, so
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'start' }"
|
|
||||||
class="bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
|
||||||
>
|
|
||||||
create your identifier.
|
|
||||||
</router-link>
|
|
||||||
<br />
|
|
||||||
If you don't that first, these contacts won't see your activity.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
|
|
||||||
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
|
|
||||||
<span>
|
|
||||||
If you do not see a scanning camera window here, check your camera
|
|
||||||
permissions.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as didJwt from "did-jwt";
|
|
||||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import * as R from "ramda";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { QrcodeStream } from "vue-qrcode-reader";
|
import { QrcodeStream } from "vue-qrcode-reader";
|
||||||
import { useClipboard } from "@vueuse/core";
|
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto";
|
import * as R from "ramda";
|
||||||
|
import { SimpleSigner } from "@/libs/crypto";
|
||||||
|
import * as didJwt from "did-jwt";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
import {
|
import {
|
||||||
@@ -106,7 +62,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
givenName = "";
|
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
@@ -120,7 +75,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Attempted to show contact info with no identity available.",
|
"Attempted to load Give records with no identity available.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
@@ -131,23 +86,24 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.givenName = settings?.firstName || "";
|
|
||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
||||||
if (account) {
|
if (!account) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "",
|
||||||
|
text: "You have no identity yet.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
const publicKeyHex = identity.keys[0].publicKeyHex;
|
const publicKeyHex = identity.keys[0].publicKeyHex;
|
||||||
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
||||||
|
|
||||||
const newDerivPath = nextDerivationPath(account.derivationPath);
|
|
||||||
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
|
|
||||||
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
|
||||||
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
|
||||||
const nextPublicEncKeyHashBase64 =
|
|
||||||
Buffer.from(nextPublicEncKeyHash).toString("base64");
|
|
||||||
|
|
||||||
const contactInfo = {
|
const contactInfo = {
|
||||||
iat: Date.now(),
|
iat: Date.now(),
|
||||||
iss: this.activeDid,
|
iss: this.activeDid,
|
||||||
@@ -156,7 +112,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
(settings?.firstName || "") +
|
(settings?.firstName || "") +
|
||||||
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
||||||
publicEncKey,
|
publicEncKey,
|
||||||
nextPublicEncKeyHash: nextPublicEncKeyHashBase64,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,7 +137,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
onScanDetect(content: any) {
|
onScanDetect(content: any) {
|
||||||
if (content[0]?.rawValue) {
|
if (content[0]?.rawValue) {
|
||||||
//console.log("onDetect", content[0].rawValue);
|
console.log("onDetect", content[0].rawValue);
|
||||||
localStorage.setItem("contactEndorserUrl", content[0].rawValue);
|
localStorage.setItem("contactEndorserUrl", content[0].rawValue);
|
||||||
this.$router.push({ name: "contacts" });
|
this.$router.push({ name: "contacts" });
|
||||||
} else {
|
} else {
|
||||||
@@ -211,22 +166,5 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyToClipboard() {
|
|
||||||
useClipboard()
|
|
||||||
.copy(this.qrValue)
|
|
||||||
.then(() => {
|
|
||||||
console.log("Contact URL:", this.qrValue);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "toast",
|
|
||||||
title: "Copied",
|
|
||||||
text: "Contact URL was copied to clipboard.",
|
|
||||||
},
|
|
||||||
2000,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Contacts"></QuickNav>
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Your Contacts
|
Your Contacts
|
||||||
@@ -9,26 +9,25 @@
|
|||||||
<div class="flex justify-between py-2">
|
<div class="flex justify-between py-2">
|
||||||
<span />
|
<span />
|
||||||
<span>
|
<span>
|
||||||
<a
|
<router-link
|
||||||
@click="showHintsForOnboarding()"
|
:to="{ name: 'help' }"
|
||||||
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
|
class="text-xs uppercase bg-blue-500 text-white px-1.5 py-1 rounded-md ml-1"
|
||||||
>
|
>
|
||||||
Onboarding Hints
|
Help
|
||||||
</a>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Contact -->
|
<!-- New Contact -->
|
||||||
<div class="mt-4 mb-4 flex items-stretch">
|
<div class="mb-4 flex">
|
||||||
<router-link
|
<span class="self-center bg-slate-500 text-white px-1.5 py-1 rounded-md">
|
||||||
:to="{ name: 'contact-qr' }"
|
<router-link :to="{ name: 'contact-qr' }">
|
||||||
class="flex items-center bg-slate-500 text-white px-1.5 py-1 mr-1 rounded-md"
|
<fa icon="qrcode" class="fa-fw" />
|
||||||
>
|
</router-link>
|
||||||
<fa icon="qrcode" class="fa-fw text-2xl" />
|
</span>
|
||||||
</router-link>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="DID, Name, Public Key, Next Public Key Hash"
|
placeholder="DID, Name, Public Key"
|
||||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
||||||
v-model="contactInput"
|
v-model="contactInput"
|
||||||
/>
|
/>
|
||||||
@@ -68,8 +67,8 @@
|
|||||||
showGiveTotals
|
showGiveTotals
|
||||||
? "Total"
|
? "Total"
|
||||||
: showGiveConfirmed
|
: showGiveConfirmed
|
||||||
? "Confirmed"
|
? "Confirmed"
|
||||||
: "Unconfirmed"
|
: "Unconfirmed"
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
<br />
|
<br />
|
||||||
@@ -109,55 +108,54 @@
|
|||||||
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
|
||||||
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
Public Key (base 64): {{ contact.publicKeyBase64 }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm truncate" v-if="contact.nextPubKeyHashB64">
|
|
||||||
Next Public Key Hash (base 64):
|
|
||||||
{{ contact.nextPubKeyHashB64 }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
<div id="ContactActions" class="flex gap-1.5 mt-2">
|
||||||
<div v-if="activeDid">
|
<button
|
||||||
<button
|
v-if="contact.seesMe"
|
||||||
v-if="contact.seesMe"
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
@click="setVisibility(contact, false)"
|
||||||
@click="setVisibility(contact, false, true)"
|
title="They can see you"
|
||||||
title="They can see you"
|
>
|
||||||
>
|
<fa icon="eye" class="fa-fw" />
|
||||||
<fa icon="eye" class="fa-fw" />
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
v-else
|
||||||
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
|
@click="setVisibility(contact, true)"
|
||||||
|
title="They cannot see you"
|
||||||
|
>
|
||||||
|
<fa icon="eye-slash" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
|
@click="checkVisibility(contact)"
|
||||||
|
title="Check Visibility"
|
||||||
|
>
|
||||||
|
<fa icon="rotate" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="register(contact)"
|
||||||
|
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
<fa
|
||||||
|
v-if="contact.registered"
|
||||||
|
icon="person-circle-check"
|
||||||
|
class="fa-fw"
|
||||||
|
title="Registered"
|
||||||
|
/>
|
||||||
|
<fa
|
||||||
v-else
|
v-else
|
||||||
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
icon="person-circle-question"
|
||||||
@click="setVisibility(contact, true, true)"
|
class="fa-fw"
|
||||||
title="They cannot see you"
|
title="Registration Unknown"
|
||||||
>
|
/>
|
||||||
<fa icon="eye-slash" class="fa-fw" />
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="text-sm uppercase bg-slate-500 text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
|
||||||
@click="checkVisibility(contact)"
|
|
||||||
title="Check Visibility"
|
|
||||||
v-if="activeDid"
|
|
||||||
>
|
|
||||||
<fa icon="rotate" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="register(contact)"
|
|
||||||
class="text-sm uppercase bg-slate-500 text-white ml-6 px-2 py-1.5 rounded-md"
|
|
||||||
v-if="activeDid"
|
|
||||||
title="Registration"
|
|
||||||
>
|
|
||||||
<fa
|
|
||||||
v-if="contact.registered"
|
|
||||||
icon="person-circle-check"
|
|
||||||
class="fa-fw"
|
|
||||||
/>
|
|
||||||
<fa v-else icon="person-circle-question" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="deleteContact(contact)"
|
@click="deleteContact(contact)"
|
||||||
class="text-sm uppercase bg-red-600 text-white ml-24 px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-red-600 text-white px-2 py-1.5 rounded-md"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
<fa icon="trash-can" class="fa-fw" />
|
<fa icon="trash-can" class="fa-fw" />
|
||||||
@@ -170,7 +168,7 @@
|
|||||||
<button
|
<button
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-l-md"
|
||||||
@click="onClickAddGive(activeDid, contact.did)"
|
@click="onClickAddGive(activeDid, contact.did)"
|
||||||
:title="givenByMeDescriptions[contact.did] || ''"
|
title="givenByMeDescriptions[contact.did]"
|
||||||
>
|
>
|
||||||
To:
|
To:
|
||||||
{{
|
{{
|
||||||
@@ -189,7 +187,7 @@
|
|||||||
<button
|
<button
|
||||||
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
|
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded-r-md -ml-1.5 border-l border-blue-400"
|
||||||
@click="onClickAddGive(contact.did, activeDid)"
|
@click="onClickAddGive(contact.did, activeDid)"
|
||||||
:title="givenToMeDescriptions[contact.did] || ''"
|
title="givenToMeDescriptions[contact.did]"
|
||||||
>
|
>
|
||||||
From:
|
From:
|
||||||
{{
|
{{
|
||||||
@@ -220,7 +218,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else>There are no contacts.</p>
|
<p v-else>This identity has no contacts.</p>
|
||||||
|
|
||||||
<div v-if="contactEdit !== null" class="dialog-overlay">
|
<div v-if="contactEdit !== null" class="dialog-overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
@@ -265,7 +263,6 @@ import {
|
|||||||
SimpleSigner,
|
SimpleSigner,
|
||||||
} from "@/libs/crypto";
|
} from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
CONTACT_URL_PREFIX,
|
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
GiveVerifiableCredential,
|
GiveVerifiableCredential,
|
||||||
RegisterVerifiableCredential,
|
RegisterVerifiableCredential,
|
||||||
@@ -275,7 +272,6 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
import { ONBOARD_MESSAGE } from "@/libs/util";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
@@ -307,7 +303,6 @@ export default class ContactsView extends Vue {
|
|||||||
givenToMeUnconfirmed: Record<string, number> = {};
|
givenToMeUnconfirmed: Record<string, number> = {};
|
||||||
hourDescriptionInput = "";
|
hourDescriptionInput = "";
|
||||||
hourInput = "0";
|
hourInput = "0";
|
||||||
isRegistered = false;
|
|
||||||
showGiveNumbers = false;
|
showGiveNumbers = false;
|
||||||
showGiveTotals = true;
|
showGiveTotals = true;
|
||||||
showGiveConfirmed = true;
|
showGiveConfirmed = true;
|
||||||
@@ -317,7 +312,6 @@ export default class ContactsView extends Vue {
|
|||||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
|
||||||
|
|
||||||
this.showGiveNumbers = !!settings?.showContactGivesInline;
|
this.showGiveNumbers = !!settings?.showContactGivesInline;
|
||||||
if (this.showGiveNumbers) {
|
if (this.showGiveNumbers) {
|
||||||
@@ -336,7 +330,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string): Promise<IIdentifier> {
|
public async getIdentity(activeDid: string) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
|
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
|
||||||
@@ -367,10 +361,6 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadGives() {
|
async loadGives() {
|
||||||
if (!this.activeDid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleResponse = (
|
const handleResponse = (
|
||||||
resp: { status: number; data: { data: GiveServerRecord[] } },
|
resp: { status: number; data: { data: GiveServerRecord[] } },
|
||||||
descriptions: Record<string, string>,
|
descriptions: Record<string, string>,
|
||||||
@@ -405,11 +395,11 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Retrieval Error",
|
title: "Server Error",
|
||||||
text:
|
text:
|
||||||
"Got an error retrieving your " +
|
"Got an error retrieving your " +
|
||||||
(useRecipient ? "given" : "received") +
|
(useRecipient ? "given" : "received") +
|
||||||
" data from the server.",
|
" time from the server.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -460,31 +450,18 @@ export default class ContactsView extends Vue {
|
|||||||
this.givenToMeConfirmed = givenToMeConfirmed;
|
this.givenToMeConfirmed = givenToMeConfirmed;
|
||||||
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error loading gives", error);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Load Error",
|
title: "Server Error",
|
||||||
text: "Got an error loading your gives.",
|
text: error as string,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showHintsForOnboarding() {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "info",
|
|
||||||
title: "Onboard Someone",
|
|
||||||
text: ONBOARD_MESSAGE,
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onClickNewContact(): Promise<void> {
|
async onClickNewContact(): Promise<void> {
|
||||||
if (!this.contactInput) {
|
if (!this.contactInput) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -498,14 +475,8 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) {
|
|
||||||
await this.newContactFromScan(this.contactInput);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let did = this.contactInput;
|
let did = this.contactInput;
|
||||||
let name, publicKeyInput, nextPublicKeyHashInput;
|
let name, publicKeyBase64;
|
||||||
const commaPos1 = this.contactInput.indexOf(",");
|
const commaPos1 = this.contactInput.indexOf(",");
|
||||||
if (commaPos1 > -1) {
|
if (commaPos1 > -1) {
|
||||||
did = this.contactInput.substring(0, commaPos1).trim();
|
did = this.contactInput.substring(0, commaPos1).trim();
|
||||||
@@ -513,32 +484,16 @@ export default class ContactsView extends Vue {
|
|||||||
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
|
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
|
||||||
if (commaPos2 > -1) {
|
if (commaPos2 > -1) {
|
||||||
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
||||||
publicKeyInput = this.contactInput.substring(commaPos2 + 1).trim();
|
publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim();
|
||||||
const commaPos3 = this.contactInput.indexOf(",", commaPos2 + 1);
|
|
||||||
if (commaPos3 > -1) {
|
|
||||||
publicKeyInput = this.contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier
|
|
||||||
nextPublicKeyHashInput = this.contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// help with potential mistakes while this sharing requires copy-and-paste
|
// help with potential mistakes while this sharing requires copy-and-paste
|
||||||
let publicKeyBase64 = publicKeyInput;
|
|
||||||
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
||||||
// it must be all hex (compressed public key), so convert
|
// it must be all hex (compressed public key), so convert
|
||||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
||||||
}
|
}
|
||||||
let nextPubKeyHashB64 = nextPublicKeyHashInput;
|
const newContact = { did, name, publicKeyBase64 };
|
||||||
if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) {
|
return this.addContact(newContact);
|
||||||
// it must be all hex (compressed public key), so convert
|
|
||||||
nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier
|
|
||||||
}
|
|
||||||
const newContact = {
|
|
||||||
did,
|
|
||||||
name,
|
|
||||||
publicKeyBase64,
|
|
||||||
nextPubKeyHashB64: nextPubKeyHashB64,
|
|
||||||
};
|
|
||||||
await this.addContact(newContact);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async newContactFromScan(url: string): Promise<void> {
|
async newContactFromScan(url: string): Promise<void> {
|
||||||
@@ -558,7 +513,6 @@ export default class ContactsView extends Vue {
|
|||||||
return this.addContact({
|
return this.addContact({
|
||||||
did: payload.iss,
|
did: payload.iss,
|
||||||
name: payload.own.name,
|
name: payload.own.name,
|
||||||
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
|
|
||||||
publicKeyBase64: payload.own.publicEncKey,
|
publicKeyBase64: payload.own.publicEncKey,
|
||||||
} as Contact);
|
} as Contact);
|
||||||
}
|
}
|
||||||
@@ -577,7 +531,6 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newContact.seesMe = true; // since we will immediately set that on the server
|
|
||||||
return db.contacts
|
return db.contacts
|
||||||
.add(newContact)
|
.add(newContact)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -586,50 +539,24 @@ export default class ContactsView extends Vue {
|
|||||||
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
||||||
allContacts,
|
allContacts,
|
||||||
);
|
);
|
||||||
let addedMessage;
|
|
||||||
if (this.activeDid) {
|
|
||||||
this.setVisibility(newContact, true, false);
|
|
||||||
addedMessage =
|
|
||||||
"They were added, and your activity is visible to them.";
|
|
||||||
} else {
|
|
||||||
addedMessage = "They were added.";
|
|
||||||
}
|
|
||||||
if (this.isRegistered) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "info",
|
|
||||||
title: "New User?",
|
|
||||||
text: "If they are a new user, be sure to register them.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Contact Added",
|
title: "Contact added",
|
||||||
text: addedMessage,
|
text: newContact.name + " was added.",
|
||||||
},
|
},
|
||||||
-1, // keeping it up so that the "visibility" message is seen
|
-1,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Error when adding contact to storage:", err);
|
console.error("Error when adding contact to storage:", err);
|
||||||
let message = "An error prevented this import.";
|
|
||||||
if (
|
|
||||||
err.message?.indexOf("Key already exists in the object store.") > -1
|
|
||||||
) {
|
|
||||||
message =
|
|
||||||
"A contact with that DID is already in your contact list. Edit them directly below.";
|
|
||||||
}
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Contact Not Added",
|
title: "Contact Not Added",
|
||||||
text: message,
|
text: "An error prevented importing.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -639,13 +566,11 @@ export default class ContactsView extends Vue {
|
|||||||
async deleteContact(contact: Contact) {
|
async deleteContact(contact: Contact) {
|
||||||
if (
|
if (
|
||||||
confirm(
|
confirm(
|
||||||
"You should first make sure that your activity is no longer visible to them." +
|
"Are you sure you want to delete " +
|
||||||
" Note that this only deletes them from your contacts on this device." +
|
|
||||||
" \n\nAre you sure you want to remove " +
|
|
||||||
this.nameForDid(this.contacts, contact.did) +
|
this.nameForDid(this.contacts, contact.did) +
|
||||||
" with DID " +
|
" with DID " +
|
||||||
contact.did +
|
contact.did +
|
||||||
" from your contact list?",
|
" ?",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -741,7 +666,6 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error when registering:", error);
|
|
||||||
let userMessage = "There was an error. See logs for more info.";
|
let userMessage = "There was an error. See logs for more info.";
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
@@ -758,7 +682,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Registration Error",
|
title: "Server Error",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
@@ -768,75 +692,48 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setVisibility(
|
async setVisibility(contact: Contact, visibility: boolean) {
|
||||||
contact: Contact,
|
const url =
|
||||||
visibility: boolean,
|
this.apiServer +
|
||||||
showSuccessAlert: boolean,
|
"/api/report/" +
|
||||||
) {
|
(visibility ? "canSeeMe" : "cannotSeeMe");
|
||||||
const visibilityPrompt =
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
showSuccessAlert &&
|
const headers = await this.getHeaders(identity);
|
||||||
(visibility
|
const payload = JSON.stringify({ did: contact.did });
|
||||||
? "Are you sure you want to make your activity visible to them?"
|
|
||||||
: "Are you sure you want to hide all your activity from them?");
|
|
||||||
if (visibilityPrompt && confirm(visibilityPrompt)) {
|
|
||||||
const url =
|
|
||||||
this.apiServer +
|
|
||||||
"/api/report/" +
|
|
||||||
(visibility ? "canSeeMe" : "cannotSeeMe");
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
|
||||||
const headers = await this.getHeaders(identity);
|
|
||||||
const payload = JSON.stringify({ did: contact.did });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
if (showSuccessAlert) {
|
contact.seesMe = visibility;
|
||||||
this.$notify(
|
db.contacts.update(contact.did, { seesMe: visibility });
|
||||||
{
|
} else {
|
||||||
group: "alert",
|
console.error(
|
||||||
type: "success",
|
"Got some bad server response when setting visibility: ",
|
||||||
title: "Visibility Set",
|
resp,
|
||||||
text:
|
);
|
||||||
this.nameForDid(this.contacts, contact.did) +
|
const message =
|
||||||
" can " +
|
resp.data.error?.message || "Bad server response of " + resp.status;
|
||||||
(visibility ? "" : "not ") +
|
|
||||||
"see your activity.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
contact.seesMe = visibility;
|
|
||||||
db.contacts.update(contact.did, { seesMe: visibility });
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"Got some bad server response when setting visibility: ",
|
|
||||||
resp.status,
|
|
||||||
resp,
|
|
||||||
);
|
|
||||||
const message =
|
|
||||||
resp.data.error?.message || "Got some error setting visibility.";
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Setting Visibility",
|
|
||||||
text: message,
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Got some error when setting visibility:", err);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Setting Visibility",
|
title: "Server Error",
|
||||||
text: "Check connectivity and try again.",
|
text: message,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Got some server error when setting visibility:", err);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Server Error",
|
||||||
|
text: "Check connectivity and try again.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,7 +756,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Visibility Refreshed",
|
title: "Refreshed",
|
||||||
text:
|
text:
|
||||||
this.nameForContact(contact, true) +
|
this.nameForContact(contact, true) +
|
||||||
" can " +
|
" can " +
|
||||||
@@ -875,19 +772,19 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Checking Visibility",
|
title: "Server Error",
|
||||||
text: message,
|
text: message,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Caught error from request to check visibility:", err);
|
console.log("Caught error from server request to check visibility:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Checking Visibility",
|
title: "Server Error",
|
||||||
text: "Check connectivity and try again.",
|
text: "Check connectivity and try again.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
@@ -946,13 +843,13 @@ export default class ContactsView extends Vue {
|
|||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
} else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) {
|
} else if (!parseFloat(this.hourInput)) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Input Error",
|
title: "Input Error",
|
||||||
text: "Giving no hours or descrption does nothing.",
|
text: "Giving 0 hours does nothing.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -1003,7 +900,6 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// similar function is in endorserServer.ts
|
|
||||||
private async createAndSubmitGive(
|
private async createAndSubmitGive(
|
||||||
identity: IIdentifier,
|
identity: IIdentifier,
|
||||||
fromDid: string,
|
fromDid: string,
|
||||||
@@ -1073,7 +969,6 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error in createAndSubmitGive: ", error);
|
|
||||||
let userMessage = "There was an error. See logs for more info.";
|
let userMessage = "There was an error. See logs for more info.";
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
@@ -1090,7 +985,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error Sending Give",
|
title: "Server Error",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
@@ -1155,10 +1050,7 @@ export default class ContactsView extends Vue {
|
|||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
||||||
Tooltip, generated on "title" attributes on "fa" icons
|
|
||||||
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
|
||||||
*/
|
|
||||||
/* Tooltip container */
|
/* Tooltip container */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Discover"></QuickNav>
|
<QuickNav selected="Discover"></QuickNav>
|
||||||
<TopMessage />
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Discover
|
Discover
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Quick Search -->
|
<!-- Quick Search -->
|
||||||
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchSelected()">
|
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="searchTerms"
|
v-model="searchTerms"
|
||||||
@@ -18,7 +17,7 @@
|
|||||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@click="searchSelected()"
|
@click="searchAll()"
|
||||||
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||||
>
|
>
|
||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||||
@@ -42,10 +41,8 @@
|
|||||||
Nearby
|
Nearby
|
||||||
<span
|
<span
|
||||||
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||||
v-if="isLocalActive"
|
>{{ localCount }}</span
|
||||||
>
|
>
|
||||||
{{ localCount > -1 ? localCount : "?" }}
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -62,24 +59,54 @@
|
|||||||
Anywhere
|
Anywhere
|
||||||
<span
|
<span
|
||||||
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||||
v-if="isRemoteActive"
|
>{{ remoteCount }}</span
|
||||||
>
|
>
|
||||||
{{ remoteCount > -1 ? remoteCount : "?" }}
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLocalActive">
|
<div v-if="isLocalActive">
|
||||||
<div>
|
<div v-if="!isChoosingSearchBox">
|
||||||
<button
|
<button
|
||||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
@click="$router.push({ name: 'search-area' })"
|
@click="isChoosingSearchBox = true"
|
||||||
>
|
>
|
||||||
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
||||||
|
Choose Location Below for Nearby Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="storeSearchBox"
|
||||||
|
>
|
||||||
|
Store This Location for Nearby Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="searchBox"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="forgetSearchBox"
|
||||||
|
>
|
||||||
|
Delete Stored Location
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="resetLatLong"
|
||||||
|
>
|
||||||
|
Reset Marker
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="cancelSearchBoxSelect"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Animation -->
|
<!-- Loading Animation -->
|
||||||
@@ -91,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<InfiniteScroll @reached-bottom="loadMoreData">
|
<InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox">
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300"
|
class="border-b border-slate-300"
|
||||||
@@ -123,11 +150,50 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isLocalActive && isChoosingSearchBox"
|
||||||
|
style="height: 600px; width: 800px"
|
||||||
|
>
|
||||||
|
<l-map
|
||||||
|
ref="map"
|
||||||
|
:center="[localCenterLat, localCenterLong]"
|
||||||
|
v-model:zoom="localZoom"
|
||||||
|
@click="setMapPoint"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
:lat-lng="[localCenterLat, localCenterLong]"
|
||||||
|
@click="isNewMarkerSet = false"
|
||||||
|
/>
|
||||||
|
<l-rectangle
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
:bounds="[
|
||||||
|
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
||||||
|
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
||||||
|
]"
|
||||||
|
:weight="1"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { LeafletMouseEvent } from "leaflet";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import {
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LRectangle,
|
||||||
|
LTileLayer,
|
||||||
|
} from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
@@ -137,7 +203,10 @@ import { didInfo, ProjectData } from "@/libs/endorserServer";
|
|||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
|
||||||
|
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||||
|
const WORLD_ZOOM = 2;
|
||||||
|
const DEFAULT_ZOOM = 2;
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -148,10 +217,13 @@ interface Notification {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
LRectangle,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
TopMessage,
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LTileLayer,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {
|
export default class DiscoverView extends Vue {
|
||||||
@@ -163,12 +235,19 @@ export default class DiscoverView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
searchTerms = "";
|
searchTerms = "";
|
||||||
projects: ProjectData[] = [];
|
projects: ProjectData[] = [];
|
||||||
isLoading = false;
|
isChoosingSearchBox = false;
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
isRemoteActive = false;
|
isRemoteActive = false;
|
||||||
localCount = -1;
|
isNewMarkerSet = false;
|
||||||
remoteCount = -1;
|
localCenterLat = 0;
|
||||||
|
localCenterLong = 0;
|
||||||
|
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localCount = 0;
|
||||||
|
localZoom = DEFAULT_ZOOM;
|
||||||
|
remoteCount = 0;
|
||||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
// make this function available to the Vue template
|
// make this function available to the Vue template
|
||||||
didInfo = didInfo;
|
didInfo = didInfo;
|
||||||
@@ -179,6 +258,7 @@ export default class DiscoverView extends Vue {
|
|||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
|
this.resetLatLong();
|
||||||
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
@@ -186,26 +266,7 @@ export default class DiscoverView extends Vue {
|
|||||||
const allAccounts = await accountsDB.accounts.toArray();
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
this.allMyDids = allAccounts.map((acc) => acc.did);
|
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||||
|
|
||||||
if (this.searchBox) {
|
this.searchLocal();
|
||||||
await this.searchLocal();
|
|
||||||
} else {
|
|
||||||
this.isLocalActive = false;
|
|
||||||
this.isRemoteActive = true;
|
|
||||||
await this.searchAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetCounts() {
|
|
||||||
this.localCount = -1;
|
|
||||||
this.remoteCount = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async searchSelected() {
|
|
||||||
if (this.isLocalActive) {
|
|
||||||
await this.searchLocal();
|
|
||||||
} else {
|
|
||||||
await this.searchAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async buildHeaders(): Promise<HeadersInit> {
|
public async buildHeaders(): Promise<HeadersInit> {
|
||||||
@@ -233,13 +294,6 @@ export default class DiscoverView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async searchAll(beforeId?: string) {
|
public async searchAll(beforeId?: string) {
|
||||||
this.resetCounts();
|
|
||||||
|
|
||||||
if (!beforeId) {
|
|
||||||
// this was an initial search so clear any previous results
|
|
||||||
this.projects = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
|
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
|
||||||
|
|
||||||
if (beforeId) {
|
if (beforeId) {
|
||||||
@@ -277,8 +331,8 @@ export default class DiscoverView extends Vue {
|
|||||||
const plans: ProjectData[] = results.data;
|
const plans: ProjectData[] = results.data;
|
||||||
if (plans) {
|
if (plans) {
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
const { name, description, handleId, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
this.projects.push({ name, description, handleId, rowid });
|
||||||
}
|
}
|
||||||
this.remoteCount = this.projects.length;
|
this.remoteCount = this.projects.length;
|
||||||
} else {
|
} else {
|
||||||
@@ -302,21 +356,13 @@ export default class DiscoverView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async searchLocal(beforeId?: string) {
|
public async searchLocal(beforeId?: string) {
|
||||||
this.resetCounts();
|
|
||||||
|
|
||||||
if (!this.searchBox) {
|
if (!this.searchBox) {
|
||||||
this.projects = [];
|
this.projects = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!beforeId) {
|
|
||||||
// this was an initial search so clear any previous results
|
|
||||||
this.projects = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const claimContents =
|
const claimContents =
|
||||||
"claimContents=" + encodeURIComponent(this.searchTerms);
|
"claimContents=" + encodeURIComponent(this.searchTerms);
|
||||||
|
|
||||||
let queryParams = [
|
let queryParams = [
|
||||||
claimContents,
|
claimContents,
|
||||||
"minLocLat=" + this.searchBox.bbox.minLat,
|
"minLocLat=" + this.searchBox.bbox.minLat,
|
||||||
@@ -360,14 +406,10 @@ export default class DiscoverView extends Vue {
|
|||||||
if (beforeId) {
|
if (beforeId) {
|
||||||
const plans: ProjectData[] = results.data;
|
const plans: ProjectData[] = results.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
const { name, description, handleId = plan.handleId, rowid } = plan;
|
||||||
this.projects.push({
|
if (beforeId !== plan["rowid"]) {
|
||||||
name,
|
this.projects.push({ name, description, handleId, rowid });
|
||||||
description,
|
}
|
||||||
handleId,
|
|
||||||
issuerDid,
|
|
||||||
rowid,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.projects = results.data;
|
this.projects = results.data;
|
||||||
@@ -415,11 +457,133 @@ export default class DiscoverView extends Vue {
|
|||||||
onClickLoadProject(id: string) {
|
onClickLoadProject(id: string) {
|
||||||
localStorage.setItem("projectId", id);
|
localStorage.setItem("projectId", id);
|
||||||
const route = {
|
const route = {
|
||||||
path: "/project/" + encodeURIComponent(id),
|
name: "project",
|
||||||
};
|
};
|
||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMapPoint(event: LeafletMouseEvent) {
|
||||||
|
if (this.isNewMarkerSet) {
|
||||||
|
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
||||||
|
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
||||||
|
} else {
|
||||||
|
// marker is not set
|
||||||
|
this.localCenterLat = event.latlng.lat;
|
||||||
|
this.localCenterLong = event.latlng.lng;
|
||||||
|
|
||||||
|
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
// Guess at a size for the bounding box.
|
||||||
|
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
||||||
|
const bounds = event.target.boxZoom?._map?.getBounds();
|
||||||
|
if (bounds) {
|
||||||
|
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
||||||
|
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
||||||
|
}
|
||||||
|
this.localLatDiff = latDiff;
|
||||||
|
this.localLongDiff = longDiff;
|
||||||
|
this.isNewMarkerSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetLatLong() {
|
||||||
|
if (this.searchBox?.bbox) {
|
||||||
|
const bbox = this.searchBox.bbox;
|
||||||
|
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
||||||
|
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
||||||
|
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
||||||
|
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
||||||
|
this.localZoom = WORLD_ZOOM;
|
||||||
|
this.isNewMarkerSet = true;
|
||||||
|
} else {
|
||||||
|
this.isNewMarkerSet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async storeSearchBox() {
|
||||||
|
if (this.localCenterLong || this.localCenterLat) {
|
||||||
|
try {
|
||||||
|
const newSearchBox = {
|
||||||
|
name: "Local",
|
||||||
|
bbox: {
|
||||||
|
eastLong: this.localCenterLong + this.localLongDiff,
|
||||||
|
maxLat: this.localCenterLat + this.localLatDiff,
|
||||||
|
minLat: this.localCenterLat - this.localLatDiff,
|
||||||
|
westLong: this.localCenterLong - this.localLongDiff,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
searchBoxes: [newSearchBox],
|
||||||
|
});
|
||||||
|
this.searchBox = newSearchBox;
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.searchLocal();
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Search Settings",
|
||||||
|
text: "Try going to a different page and then coming back.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to retry the location search setting because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "No Location Selected",
|
||||||
|
text: "Select a location on the map.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async forgetSearchBox() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
searchBoxes: [],
|
||||||
|
});
|
||||||
|
this.searchBox = null;
|
||||||
|
this.localCenterLat = 0;
|
||||||
|
this.localCenterLong = 0;
|
||||||
|
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
this.localZoom = DEFAULT_ZOOM;
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.isNewMarkerSet = false;
|
||||||
|
this.searchLocal();
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Search Settings",
|
||||||
|
text: "Try going to a different page and then coming back.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to retry the location search setting because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelSearchBoxSelect() {
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.localZoom = WORLD_ZOOM;
|
||||||
|
}
|
||||||
|
|
||||||
public computedLocalTabClassNames() {
|
public computedLocalTabClassNames() {
|
||||||
return {
|
return {
|
||||||
"inline-block": true,
|
"inline-block": true,
|
||||||
|
|||||||
@@ -1,417 +0,0 @@
|
|||||||
<template>
|
|
||||||
<QuickNav />
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
||||||
<!-- Breadcrumb -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<!-- Back -->
|
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Notification Help
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- eslint-disable prettier/prettier -->
|
|
||||||
<div>
|
|
||||||
<p>Here are ways to test notifications and get them working.</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">Full Test</h2>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
If this works then you're all set.
|
|
||||||
<button
|
|
||||||
@click="sendTestWebPushMessage(true)"
|
|
||||||
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
|
||||||
>
|
|
||||||
Send Yourself a Test Web Push Message (Through Push Server but
|
|
||||||
Skipping Client Filter)
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">
|
|
||||||
If this app doesn't support notifications...
|
|
||||||
<!-- Note that that exact verbiage shows in a message elsewhere. -->
|
|
||||||
</h2>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
To be notified of interesting updates, install this app on your device
|
|
||||||
(as opposed to using it inside the browser app). In Chrome, it may prompt
|
|
||||||
you, and you can also look for the "Install" command in the browser
|
|
||||||
settings; on the the desktop, look for this icon in the address bar:
|
|
||||||
<img
|
|
||||||
src="../assets/help/chrome-install-pwa.png"
|
|
||||||
alt="Chrome 'install' icon"
|
|
||||||
class="ml-4"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">
|
|
||||||
If you must enable notifications...
|
|
||||||
<!-- Note that that exact verbiage shows in a message elsewhere. -->
|
|
||||||
</h2>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
<button class="text-blue-500" @click="showNotificationChoice()">
|
|
||||||
Click here.
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">
|
|
||||||
If you're waiting for system initialization...
|
|
||||||
<!-- Note that that exact verbiage shows in a message elsewhere. -->
|
|
||||||
</h2>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
... and it never stops, then there is a problem with the underlying
|
|
||||||
service worker or push server mechanism in your browser. Your best bet
|
|
||||||
is to follow the "Reinstall" steps below or use a different browser.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">Check App Permissions</h2>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
In Apple iOS, check "Settings" -> "Notifications", look for the Time
|
|
||||||
Safari app (or the browser you're using), and make sure notifications
|
|
||||||
are enabled.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In Android, hold on to the app icon, then select "App Info", then
|
|
||||||
"Notifications" and make sure they're enabled. If it's still a problem
|
|
||||||
then go further:
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If you installed the app with Chrome, make sure there are no other
|
|
||||||
tabs with it open. Here are some ways to clear caches that can mess
|
|
||||||
things up (and note that this clears out data from the installed app
|
|
||||||
-- which is good to do while the app is installed):
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li class="list-disc ml-4">
|
|
||||||
Go to Chrome "App Info", then "Storage & Cache" and "Clear Storage".
|
|
||||||
</li>
|
|
||||||
<li class="list-disc ml-4">
|
|
||||||
Go to Chrome "Settings", then "Privacy and Security" and "Clear
|
|
||||||
"Clear browsing data", then "Cookies and site data". Make sure the
|
|
||||||
"Time Range" at the top shows "All time".
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
On a Mac, go to "Settings" and check "Notifications".
|
|
||||||
<img
|
|
||||||
src="../assets/help/mac-installed-app-settings.png"
|
|
||||||
alt="Mac app settings"
|
|
||||||
class="ml-4"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">Check Browser Permissions</h2>
|
|
||||||
<div>
|
|
||||||
<p>In Apple iOS, check Settings -> Notifications.</p>
|
|
||||||
<p>In Android, check Settings -> Notifications.</p>
|
|
||||||
|
|
||||||
You can find more details about compatibility
|
|
||||||
<a
|
|
||||||
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
|
|
||||||
class="text-blue-500"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">
|
|
||||||
Check Operating System (OS) Permissions
|
|
||||||
</h2>
|
|
||||||
<div class="px-2">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-semibold">Mobile Phone - Apple iOS</h3>
|
|
||||||
<div>
|
|
||||||
Notifications require iOS 16.4 or higher. To check your iOS version,
|
|
||||||
go to Settings > General > About > Software Version.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-lg font-semibold">Mobile Phone - Google Android</h3>
|
|
||||||
<div>
|
|
||||||
We recommend Chrome. It must be version 42 or higher. Check your
|
|
||||||
version under Settings -> About Chrome.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-lg font-semibold">Desktop - Mac</h3>
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
See "System Settings" -> "Notifications" and make sure it is
|
|
||||||
enabled for the browser you're using. Note that these
|
|
||||||
notifications require Mac OS 13; see your macOS version under
|
|
||||||
Apple -> "About This Mac".
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-lg font-semibold">Desktop - Windows</h3>
|
|
||||||
In Windows, check "Settings" -> "Notifications".
|
|
||||||
<img
|
|
||||||
src="../assets/help/windows-system-enable-notifications.png"
|
|
||||||
alt="Windows system settings"
|
|
||||||
class="ml-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
You can find more details about compatibility
|
|
||||||
<a
|
|
||||||
href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API#browser_compatibility"
|
|
||||||
class="text-blue-500"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">Reinstall</h2>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
If all else fails, uninstall the app, ensure all the browser tabs with
|
|
||||||
it are closed, and clear out caches and storage.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Of course, you'll want to back up all your data first -- all seeds as
|
|
||||||
well as the contacts & settings -- on the Account
|
|
||||||
<fa icon="circle-user" /> page.
|
|
||||||
</p>
|
|
||||||
<ul class="ml-4 list-disc">
|
|
||||||
<li>
|
|
||||||
Clear cache.
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
In mobile, look for the browser app settings. This is true even
|
|
||||||
for an installed app: go to the browser which you used to
|
|
||||||
initially visit timesafari.app, because those settings affect
|
|
||||||
the app. Look for "Delete browsing data" in the "Settings",
|
|
||||||
under "Privacy and Security".
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
In Chrome, go to `chrome://settings/cookies` and "all site data
|
|
||||||
and permissions" for timesafari.app; in Firefox, go to
|
|
||||||
`about:preferences` and search for "cache" then "Manage Data"
|
|
||||||
for timesafari.app. Also manually remove the IndexedDB data if
|
|
||||||
the DBs still show.)
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Clear notification permission. (In Chrome, go to
|
|
||||||
`chrome://settings/content/notifications`; in Firefox, go to
|
|
||||||
`about:preferences` and search for "notifications".)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Unregister service worker. (In Chrome, go to
|
|
||||||
`chrome://serviceworker-internals/`; in Firefox, go to
|
|
||||||
`about:serviceworkers`.)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Clear "Cache Storage". (In Chrome, in dev tools under "Application";
|
|
||||||
in Firefox, in dev tools under "Storage".)
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>Then reinstall the app.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">Tests</h2>
|
|
||||||
<button
|
|
||||||
@click="showTestNotification()"
|
|
||||||
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
|
||||||
>
|
|
||||||
Send Test Notification Directly to Device (Not Through Push Server)
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
If that didn't show a notification on your device, the problem is that
|
|
||||||
your browser or your operating system are not allowing notifications
|
|
||||||
through. See "Check App Permissions" and "Check Browser Permissions" and
|
|
||||||
"Check Operating System (OS) Permissions" above.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="alertWebPushSubscription()"
|
|
||||||
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
|
||||||
>
|
|
||||||
Show Web Push Subscription Info
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
If that showed "null" then the notification is not active.
|
|
||||||
<button class="text-blue-500" @click="showNotificationChoice()">
|
|
||||||
Click here.
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="sendTestWebPushMessage(true)"
|
|
||||||
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
|
||||||
>
|
|
||||||
Send Yourself a Test Web Push Message (Through Push Server but Skipping
|
|
||||||
Client Filter)
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
If that didn't show a notification on your device, there is a problem
|
|
||||||
getting to the push server. Disable notifications and then enable them
|
|
||||||
again.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="sendTestWebPushMessage()"
|
|
||||||
class="block w-full text-center text-md bg-slate-500 text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
|
||||||
>
|
|
||||||
Send Yourself a Test Web Push Message (Through Push Server and Client
|
|
||||||
Filter)
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
If you don't see a message, it could be that there is nothing new for
|
|
||||||
you to see. If the previous test worked, then things should work fine.
|
|
||||||
If you notice a full 24 hours where you get no notification and you know
|
|
||||||
that there are new items that should show, gather as many details as
|
|
||||||
possible and go to the bottom of
|
|
||||||
<router-link to="help" class="text-blue-500"> this page </router-link>
|
|
||||||
for ways to contact us.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- eslint-enable -->
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
|
||||||
import { sendTestThroughPushServer } from "@/libs/util";
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
group: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
|
||||||
export default class HelpNotificationsView extends Vue {
|
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
subscription: PushSubscription | null = null;
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
try {
|
|
||||||
const registration = await navigator.serviceWorker.ready;
|
|
||||||
this.subscription = await registration.pushManager.getSubscription();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Mount error:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alertWebPushSubscription() {
|
|
||||||
console.log(
|
|
||||||
"Web push subscription:",
|
|
||||||
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
|
|
||||||
);
|
|
||||||
alert(JSON.stringify(this.subscription));
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendTestWebPushMessage(skipFilter: boolean = false) {
|
|
||||||
if (!this.subscription) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Not Subscribed",
|
|
||||||
// Note that this exact verbiage shows in help text.
|
|
||||||
text: "You must enable notifications before testing the web push.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sendTestThroughPushServer(this.subscription, skipFilter);
|
|
||||||
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Test Web Push Sent",
|
|
||||||
text:
|
|
||||||
"Check your device for the test web push message" +
|
|
||||||
(skipFilter ? "." : " if there are new items in your feed."),
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Got an error sending test notification:", error);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Sending Test",
|
|
||||||
text: "Got an error sending the test web push notification.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showTestNotification() {
|
|
||||||
const TEST_NOTIFICATION_TITLE = "It Worked";
|
|
||||||
navigator.serviceWorker.ready
|
|
||||||
.then((registration) => {
|
|
||||||
return registration.showNotification(TEST_NOTIFICATION_TITLE, {
|
|
||||||
body: "This is your test notification.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Sent",
|
|
||||||
text: `A notification was triggered, so one should show on your device entitled '${TEST_NOTIFICATION_TITLE}'.`,
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Got a notification error:", error);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Failed",
|
|
||||||
text: "Got an error sending a notification.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showNotificationChoice() {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "modal",
|
|
||||||
type: "notification-permission",
|
|
||||||
title: "", // unused, only here to satisfy type check
|
|
||||||
text: "", // unused, only here to satisfy type check
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,47 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Heading -->
|
||||||
<div class="mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
<!-- Back -->
|
Help
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
</h1>
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Help
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- eslint-disable prettier/prettier -->
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
This app is a window into data that you and your friends own, focused on
|
This app is a window into data that you and your friends own, focused on
|
||||||
gifts and collaboration.
|
gifts and collaboration.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What is the idea here?</h2>
|
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
|
||||||
<p>
|
<p>
|
||||||
We are building networks of people who want to grow a giving society.
|
We are building networks of people who want to grow a gifting society.
|
||||||
First of all, you can see what people have given, and also recognize
|
First of all, you can record ways you've seen people give, and that
|
||||||
gifts you've seen, in a way that leaves a permanent record -- one that
|
leaves a permanent record -- one that came from you, and the recipient
|
||||||
came from you, and the recipient can prove it was for them. This is
|
can prove it was for them. This is personally gratifying, but it extends
|
||||||
personally gratifying, but it extends to broader work: volunteers get
|
to broader work: volunteers can get confirmation of activity and
|
||||||
confirmation of activity, and selectively show off their contributions
|
selectively show off their contributions and network.
|
||||||
and network.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can show giving and also offer help to ideas, based on others'
|
You can also record projects and plans and invite others to collaborate.
|
||||||
willingness to help out, too. You can record your own ideas and invite
|
Soon you'll be able to see when others are interested and see how much
|
||||||
others to collaborate.
|
they're willing to contribute, even if there are conditions.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This app uses the power of cryptography to build a reputation, recording
|
This app uses the power of cryptography to build a reputation, recording
|
||||||
@@ -52,39 +36,22 @@
|
|||||||
the control; this app gives you the control.
|
the control; this app gives you the control.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I get started?</h2>
|
<h2 class="text-xl font-semibold">How do I take my first action?</h2>
|
||||||
<p>
|
<p>
|
||||||
You need someone to register you -- usually the person who told you
|
You need someone to register you -- usually the person who told you
|
||||||
about this app, on the Contacts
|
about this app, on the Contacts
|
||||||
<fa icon="users" class="fa-fw" /> page. After they register you, you can
|
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
|
||||||
select any contact on the home page (or "anonymous") and record your
|
you can select any contact on the home page (or "anonymous") and record
|
||||||
appreciation for... whatever. The main goal is to record what people
|
your appreciation for... whatever. The main goal is to record what
|
||||||
have given you, to grow giving economies. Each claim is recorded on a
|
people have given you, to grow gifting economies. Each claim is recorded
|
||||||
custom ledger. The day after being registered, you'll be able to able to
|
on a custom ledger. The day after being registered, you'll be able to
|
||||||
register others; later, you can create projects, too.
|
able to register others; later, you can create projects, too.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Note that there are limits to how many others each person can register,
|
Note that there are limits to how many others each person can register,
|
||||||
so you may have to wait.
|
so you may have to wait.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I add someone else?</h2>
|
|
||||||
<p>
|
|
||||||
<button class="text-blue-500" @click="showOnboardInfo">
|
|
||||||
Click here to show an alert with the steps.
|
|
||||||
</button>
|
|
||||||
To start scanning, go
|
|
||||||
<router-link class="text-blue-500" to="/contact-qr">here.</router-link>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If they are not nearby to scan QR codes, tell them to copy their ID from
|
|
||||||
their Identity <fa icon="circle-user" class="fa-fw" /> page, which
|
|
||||||
typically starts with "did:ethr:...", and send it to you. Go to the
|
|
||||||
Contacts <fa icon="users" class="fa-fw" /> page and enter that into the
|
|
||||||
top form. To add a name, put a comma and then their name; to add their
|
|
||||||
public key, put another comma followed by the key.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
|
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
|
||||||
<p>
|
<p>
|
||||||
There are two sets of data to backup: the identifier secrets and the
|
There are two sets of data to backup: the identifier secrets and the
|
||||||
@@ -95,7 +62,7 @@
|
|||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I backup my identifier (secret) data?
|
How do I backup my identifier (secret) data?
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-inside">
|
||||||
<li>
|
<li>
|
||||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
||||||
</li>
|
</li>
|
||||||
@@ -111,7 +78,7 @@
|
|||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I backup my other (non-identifier-secret) data?
|
How do I backup my other (non-identifier-secret) data?
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-inside">
|
||||||
<li>
|
<li>
|
||||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
||||||
</li>
|
</li>
|
||||||
@@ -133,7 +100,7 @@
|
|||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I restore my identifier (secret) data?
|
How do I restore my identifier (secret) data?
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-inside">
|
||||||
<li>
|
<li>
|
||||||
<router-link class="text-blue-500" to="/import-account">
|
<router-link class="text-blue-500" to="/import-account">
|
||||||
Go to the import page
|
Go to the import page
|
||||||
@@ -145,65 +112,33 @@
|
|||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
How do I restore my other (non-identifier-secret) data?
|
How do I restore my other (non-identifier-secret) data?
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-inside">
|
||||||
<li>
|
<li>Make sure you have your backup file (above), then contact us.</li>
|
||||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
|
|
||||||
click Advanced, and follow the instructions for Data Import.
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
How do I add someone to my contacts?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Tell them to copy their ID, which typically starts with "did:ethr:...",
|
||||||
|
and send it to you. Go to the Contacts
|
||||||
|
<fa icon="circle-user" class="fa-fw" /> page and enter that into the top
|
||||||
|
form. You may add a name by adding a comma followed by their name; you
|
||||||
|
may also add their public key by adding another comma followed by the
|
||||||
|
key.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
||||||
<p>
|
<p>
|
||||||
Before doing this, note that it is an advanced feature that affects
|
Before doing this, note that it is an advanced feature that affects
|
||||||
functionality (eg. the words "Alt ID" next to results, backup features)
|
functionality (eg. the words "Alt ID" next to results, backup features)
|
||||||
so beware. You can
|
so beware if you think that may cause confusion. You can
|
||||||
<router-link to="start" class="text-blue-500">
|
<router-link to="start" class="text-blue-500">
|
||||||
create another identity here.
|
create another identity here.
|
||||||
</router-link>
|
</router-link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I erase my data?</h2>
|
|
||||||
<p>
|
|
||||||
Before doing this, note the two kinds of data to backup: identity data,
|
|
||||||
and other data for contacts and settings (see instructions above).
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li class="list-disc list-outside ml-4">
|
|
||||||
Mobile
|
|
||||||
<ul>
|
|
||||||
<li class="list-disc list-outside ml-4">
|
|
||||||
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
|
|
||||||
</li>
|
|
||||||
<li class="list-disc list-outside ml-4">
|
|
||||||
DuckDuckGo: long hold -> Clear Data (takes effect immediately)
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="list-disc list-outside ml-4">
|
|
||||||
Desktop
|
|
||||||
<ul>
|
|
||||||
<li class="list-disc list-outside ml-4">
|
|
||||||
Chrome:
|
|
||||||
<a href="chrome://settings/content/all" class="text-blue-500"
|
|
||||||
>clear here</a
|
|
||||||
>
|
|
||||||
also clear under dev tools Application
|
|
||||||
</li>
|
|
||||||
<li class="list-disc list-outside ml-4">
|
|
||||||
Firefox: <a href="about:preferences">go here</a>, Manage Data,
|
|
||||||
find timesafari.app and select, hit Remove Selected, then Save
|
|
||||||
Changes
|
|
||||||
</li>
|
|
||||||
<li class="list-disc list-outside ml-4">
|
|
||||||
Safari: Settings -> Privacy -> Manage Website Data, search for
|
|
||||||
timesafari.app and select, hit Remove Selected, then Done.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>To erase your data from our servers, contact us (below).</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
I know there is a record from someone, so why can't I see that info?
|
I know there is a record from someone, so why can't I see that info?
|
||||||
</h2>
|
</h2>
|
||||||
@@ -216,121 +151,43 @@
|
|||||||
<fa icon="eye-slash" class="fa-fw" />.
|
<fa icon="eye-slash" class="fa-fw" />.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Sometimes the reason you don't see something is because the search
|
Sometimes the reason you don't see something is because the search time
|
||||||
results are limited. Go to the bottom and make sure to load all the data
|
is limited. Go to the bottom and make sure to load all the data on a
|
||||||
on a list. If you still don't see it, try a search or view on a
|
list. If you still don't see it, try a search or view on a different
|
||||||
different page.
|
page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">What is your privacy policy?</h2>
|
||||||
Where do I get help with notifications?
|
|
||||||
</h2>
|
|
||||||
<p>
|
<p>
|
||||||
<router-link class="text-blue-500" to="/help-notifications"
|
See
|
||||||
>Here.</router-link
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
|
||||||
How do I access even more functionality?
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
There is an "Advanced" section at the bottom of the Account
|
|
||||||
<fa icon="circle-user" /> page.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
There is a even more functionality in a mobile app (and more
|
|
||||||
documentation) at
|
|
||||||
<a href="https://endorser.ch" class="text-blue-500">
|
|
||||||
EndorserSearch.com
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
|
|
||||||
<p style="display:inline; align-items: center">
|
|
||||||
This work is marked with
|
|
||||||
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
|
|
||||||
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
|
||||||
<img
|
|
||||||
src="../assets/help/creative-commons-circle.svg"
|
|
||||||
alt="CC circle"
|
|
||||||
width="20"
|
|
||||||
class="display: inline"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="../assets/help/creative-commons-zero.svg"
|
|
||||||
alt="CC zero"
|
|
||||||
width="20"
|
|
||||||
style="display: inline"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<br />
|
|
||||||
For notifications, this service stores push token data; that can be revoked at any time
|
|
||||||
by disabling notifications on the Account <fa icon="circle-user" class="fa-fw" /> page.
|
|
||||||
<br />
|
|
||||||
For all other claim data,
|
|
||||||
<a href="https://endorser.ch/privacy-policy" class="text-blue-500">
|
<a href="https://endorser.ch/privacy-policy" class="text-blue-500">
|
||||||
the Endorser Service has this Privacy Policy.
|
the Endorser Service Privacy Policy.
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">Where can I read more?</h2>
|
|
||||||
<p>
|
|
||||||
This is part of the
|
|
||||||
<a href="https://livesofgiving.org" class="text-blue-500">
|
|
||||||
Lives of Giving
|
|
||||||
</a>
|
|
||||||
initiative.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What app version is this?</h2>
|
<h2 class="text-xl font-semibold">What app version is this?</h2>
|
||||||
<p>{{ package.version }} ({{ commitHash }})</p>
|
<p>
|
||||||
|
{{ package.version }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
For any other questions, including removing your data:
|
For any other questions, including remove your data:
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Contact us at
|
Contact us through
|
||||||
<a href="mailto:info@TimeSafari.app" class="text-blue-500"
|
<a href="https://communitycred.org">CommunityCred.org</a>.
|
||||||
>info@TimeSafari.app</a
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint enable -->
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
|
|
||||||
import * as Package from "../../package.json";
|
import * as Package from "../../package.json";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import { ONBOARD_MESSAGE } from "@/libs/util";
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
group: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
export default class Help extends Vue {
|
export default class Help extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
package = Package;
|
package = Package;
|
||||||
commitHash = process.env.VUE_APP_GIT_HASH;
|
|
||||||
|
|
||||||
showOnboardInfo() {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "info",
|
|
||||||
title: "Onboard Someone",
|
|
||||||
text: ONBOARD_MESSAGE,
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,103 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Home"></QuickNav>
|
<QuickNav selected="Home"></QuickNav>
|
||||||
<TopMessage />
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Time Safari
|
Time Safari
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- prompt to install notifications -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<div
|
|
||||||
v-if="!notificationsSupported()"
|
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
|
||||||
>
|
|
||||||
<p style="display: inline; align-items: center">
|
|
||||||
This app doesn't support notifications, so let's fix that. <br />
|
|
||||||
<!-- Note that that exact verbiage shows in the help. -->
|
|
||||||
|
|
||||||
<span v-if="userAgentInfo.getOS().name === 'iOS'">
|
|
||||||
Tap on "Share"<img
|
|
||||||
src="../assets/help/apple-share-icon.svg"
|
|
||||||
alt="Apple 'share' icon"
|
|
||||||
width="30"
|
|
||||||
style="display: inline; margin: 0 5px; vertical-align: middle"
|
|
||||||
/>and then "Add to Home Screen"
|
|
||||||
<fa icon="square-plus" title="Apple 'Add' icon" />
|
|
||||||
and go click on that new app.
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else-if="userAgentInfo.getBrowser().name.startsWith('Chrome')"
|
|
||||||
>
|
|
||||||
You should see a prompt to install, or you can click on the
|
|
||||||
top-right dots
|
|
||||||
<fa
|
|
||||||
icon="ellipsis-vertical"
|
|
||||||
title="vertical ellipsis"
|
|
||||||
class="fa-fw"
|
|
||||||
/>
|
|
||||||
and then "Install"<img
|
|
||||||
src="../assets/help/install-android-chrome.png"
|
|
||||||
alt="Android 'install' icon"
|
|
||||||
width="30"
|
|
||||||
style="display: inline; margin: 0 5px; vertical-align: middle"
|
|
||||||
/>
|
|
||||||
and go use that app. If you already did these steps, reload this app
|
|
||||||
so that it is fully detected.
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
Try
|
|
||||||
<a href="https://www.google.com/chrome/" class="text-blue-500"
|
|
||||||
>Google Chrome</a
|
|
||||||
>
|
|
||||||
or look for a way to install as an app.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- show the actions for recognizing a give -->
|
<!-- show the actions for recognizing a give -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div
|
<div v-if="!activeDid">
|
||||||
v-if="!activeDid"
|
To record others' giving,
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
||||||
>
|
create your identifier.</router-link
|
||||||
<p class="text-lg mb-3">
|
|
||||||
You need an <b>identifier</b> before you can record anyone's gives.
|
|
||||||
</p>
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'start' }"
|
|
||||||
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
|
||||||
>
|
|
||||||
Create Your Identifier</router-link
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-else-if="!isRegistered">
|
||||||
v-else-if="!isRegistered"
|
To record others' giving, someone must register your account, so show
|
||||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
them
|
||||||
>
|
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
|
||||||
Someone must register your account before you can record anyone's gives.
|
your identity info</router-link
|
||||||
To do this:
|
|
||||||
<router-link
|
|
||||||
:to="{ name: 'contact-qr' }"
|
|
||||||
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
|
||||||
>
|
>
|
||||||
1. Show Them Your Identity Info</router-link
|
and then
|
||||||
>
|
<router-link :to="{ name: 'account' }" class="text-blue-500">
|
||||||
<router-link
|
check your limits.</router-link
|
||||||
:to="{ name: 'account' }"
|
|
||||||
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
|
|
||||||
>
|
|
||||||
2. Check Your Limits</router-link
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- activeDid && isRegistered -->
|
<!-- activeDid && isRegistered -->
|
||||||
<h2 class="text-xl font-bold mb-4">Record Something Given</h2>
|
<h2 class="text-xl font-bold">Record a Gift</h2>
|
||||||
|
|
||||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
<li @click="openDialog()">
|
<li @click="openDialog()">
|
||||||
@@ -149,60 +81,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiftedDialog
|
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
|
||||||
ref="customDialog"
|
|
||||||
message="Received from"
|
|
||||||
showGivenToUser="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Results List -->
|
|
||||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||||
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
||||||
<InfiniteScroll @reached-bottom="loadMoreGives">
|
|
||||||
<ul class="border-t border-slate-300">
|
|
||||||
<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 pb-2 mb-2 font-bold uppercase text-sm"
|
|
||||||
v-if="record.jwtId == feedLastViewedClaimId"
|
|
||||||
>
|
|
||||||
You've seen all the following
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex">
|
|
||||||
<fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
|
|
||||||
<span class="">{{ this.giveDescription(record) }}</span>
|
|
||||||
<a @click="onClickLoadClaim(record.jwtId)">
|
|
||||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</InfiniteScroll>
|
|
||||||
<div :class="{ hidden: isHiddenSpinner }">
|
<div :class="{ hidden: isHiddenSpinner }">
|
||||||
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||||
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
<fa icon="spinner" class="fa-spin-pulse"></fa> Loading…
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ul class="border-t border-slate-300">
|
||||||
|
<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 pb-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="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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { UAParser } from "ua-parser-js";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
|
||||||
import { db, accountsDB } from "@/db/index";
|
import { db, accountsDB } from "@/db/index";
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
import { Contact } from "@/db/tables/contacts";
|
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import {
|
import {
|
||||||
@@ -210,7 +124,11 @@ import {
|
|||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -220,13 +138,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||||
GiftedDialog,
|
|
||||||
QuickNav,
|
|
||||||
EntityIcon,
|
|
||||||
InfiniteScroll,
|
|
||||||
TopMessage,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {
|
export default class HomeView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -235,13 +147,13 @@ export default class HomeView extends Vue {
|
|||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
feedAllLoaded = false;
|
||||||
feedData = [];
|
feedData = [];
|
||||||
feedPreviousOldestId?: string;
|
feedPreviousOldestId?: string;
|
||||||
feedLastViewedClaimId?: string;
|
feedLastViewedId?: string;
|
||||||
isHiddenSpinner = true;
|
isHiddenSpinner = true;
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
|
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
@@ -255,7 +167,13 @@ export default class HomeView extends Vue {
|
|||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first()) as Account;
|
.first()) as Account;
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
return identity; // may be null
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load Give records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHeaders(identity: IIdentifier) {
|
public async getHeaders(identity: IIdentifier) {
|
||||||
@@ -278,15 +196,11 @@ export default class HomeView extends Vue {
|
|||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
this.feedLastViewedClaimId = settings?.lastViewedClaimId;
|
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
|
|
||||||
// this returns a Promise but we don't need to wait for it
|
|
||||||
this.updateAllFeed();
|
this.updateAllFeed();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log("Error retrieving settings and/or feed.", err);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -294,17 +208,13 @@ export default class HomeView extends Vue {
|
|||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
err.userMessage ||
|
err.userMessage ||
|
||||||
"There was an error retrieving your settings and/or the latest activity.",
|
"There was an error retrieving the latest sweet, sweet action.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationsSupported() {
|
|
||||||
return "Notification" in window;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async buildHeaders() {
|
public async buildHeaders() {
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -331,33 +241,24 @@ export default class HomeView extends Vue {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Data loader used by infinite scroller
|
|
||||||
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
|
||||||
**/
|
|
||||||
public async loadMoreGives(payload: boolean) {
|
|
||||||
if (payload) {
|
|
||||||
this.updateAllFeed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updateAllFeed() {
|
public async updateAllFeed() {
|
||||||
this.isHiddenSpinner = false;
|
this.isHiddenSpinner = false;
|
||||||
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
|
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
|
||||||
.then(async (results) => {
|
.then(async (results) => {
|
||||||
if (results.data.length > 0) {
|
if (results.data.length > 0) {
|
||||||
this.feedData = this.feedData.concat(results.data);
|
this.feedData = this.feedData.concat(results.data);
|
||||||
|
this.feedAllLoaded = results.hitLimit;
|
||||||
this.feedPreviousOldestId =
|
this.feedPreviousOldestId =
|
||||||
results.data[results.data.length - 1].jwtId;
|
results.data[results.data.length - 1].jwtId;
|
||||||
// The following update is only done on the first load.
|
|
||||||
if (
|
if (
|
||||||
this.feedLastViewedClaimId == null ||
|
this.feedLastViewedId == null ||
|
||||||
this.feedLastViewedClaimId < results.data[0].jwtId
|
this.feedLastViewedId < results.data[0].jwtId
|
||||||
) {
|
) {
|
||||||
await db.open();
|
await db.open();
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
lastViewedClaimId: results.data[0].jwtId,
|
lastViewedClaimId: results.data[0].jwtId,
|
||||||
});
|
});
|
||||||
|
// but not for this page because we need to remember what it was before
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -367,22 +268,17 @@ export default class HomeView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Feed Error",
|
title: "Export Error",
|
||||||
text: e.userMessage || "There was an error retrieving feed data.",
|
text: e.userMessage || "There was an error retrieving feed data.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isHiddenSpinner = true;
|
this.isHiddenSpinner = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
|
||||||
* Retrieve claims in reverse chronological order
|
|
||||||
*
|
|
||||||
* @param beforeId the earliest ID (of previous searches) to search earlier
|
|
||||||
* @return claims in reverse chronological order
|
|
||||||
*/
|
|
||||||
public async retrieveGives(endorserApiServer: string, beforeId?: string) {
|
|
||||||
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
|
||||||
@@ -447,13 +343,6 @@ export default class HomeView extends Vue {
|
|||||||
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickLoadClaim(jwtId: string) {
|
|
||||||
const route = {
|
|
||||||
path: "/claim/" + encodeURIComponent(jwtId),
|
|
||||||
};
|
|
||||||
this.$router.push(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
displayAmount(code: string, amt: number) {
|
displayAmount(code: string, amt: number) {
|
||||||
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
|
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -21,6 +21,9 @@
|
|||||||
<div class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4">
|
<div class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4">
|
||||||
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
||||||
<span class="overflow-hidden">
|
<span class="overflow-hidden">
|
||||||
|
<h2 class="text-xl font-semibold mb-0">
|
||||||
|
{{ givenName }}
|
||||||
|
</h2>
|
||||||
<div class="text-sm text-slate-500 truncate">
|
<div class="text-sm text-slate-500 truncate">
|
||||||
<b>ID:</b> <code>{{ activeDid }}</code>
|
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,6 +90,7 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
public activeDid = "";
|
public activeDid = "";
|
||||||
public apiServer = "";
|
public apiServer = "";
|
||||||
public apiServerInput = "";
|
public apiServerInput = "";
|
||||||
|
public givenName = "";
|
||||||
public otherIdentities: Array<{ did: string }> = [];
|
public otherIdentities: Array<{ did: string }> = [];
|
||||||
public showContactGives = false;
|
public showContactGives = false;
|
||||||
|
|
||||||
@@ -107,6 +111,9 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.apiServerInput = settings?.apiServer || "";
|
this.apiServerInput = settings?.apiServer || "";
|
||||||
|
this.givenName =
|
||||||
|
(settings?.firstName || "") +
|
||||||
|
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
|
||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
const identity = await this.getIdentity(this.activeDid);
|
const identity = await this.getIdentity(this.activeDid);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -76,7 +76,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|||||||
components: {},
|
components: {},
|
||||||
})
|
})
|
||||||
export default class ImportAccountView extends Vue {
|
export default class ImportAccountView extends Vue {
|
||||||
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; // for legacy imports, likely never used
|
UPORT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
|
||||||
|
|
||||||
mnemonic = "";
|
mnemonic = "";
|
||||||
address = "";
|
address = "";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -72,7 +72,6 @@ import {
|
|||||||
DEFAULT_ROOT_DERIVATION_PATH,
|
DEFAULT_ROOT_DERIVATION_PATH,
|
||||||
deriveAddress,
|
deriveAddress,
|
||||||
newIdentifier,
|
newIdentifier,
|
||||||
nextDerivationPath,
|
|
||||||
} from "../libs/crypto";
|
} from "../libs/crypto";
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
@@ -122,7 +121,17 @@ export default class ImportAccountView extends Vue {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// increment the last number in that max derivation path
|
// increment the last number in that max derivation path
|
||||||
const newDerivPath = nextDerivationPath(accountWithMaxDeriv.derivationPath);
|
let lastStr = accountWithMaxDeriv.derivationPath.split("/").slice(-1)[0];
|
||||||
|
if (lastStr.endsWith("'")) {
|
||||||
|
lastStr = lastStr.slice(0, -1);
|
||||||
|
}
|
||||||
|
const lastNum = parseInt(lastStr, 10);
|
||||||
|
const newLastNum = lastNum + 1;
|
||||||
|
const newDerivPath = accountWithMaxDeriv.derivationPath
|
||||||
|
.split("/")
|
||||||
|
.slice(0, -1)
|
||||||
|
.concat([newLastNum.toString() + "'"])
|
||||||
|
.join("/");
|
||||||
|
|
||||||
const mne: string = accountWithMaxDeriv.mnemonic;
|
const mne: string = accountWithMaxDeriv.mnemonic;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -68,7 +68,7 @@ export default class NewEditAccountView extends Vue {
|
|||||||
});
|
});
|
||||||
localStorage.setItem("firstName", this.givenName as string);
|
localStorage.setItem("firstName", this.givenName as string);
|
||||||
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
|
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
|
||||||
this.$router.back();
|
this.$router.push({ name: "account" });
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickCancel() {
|
onClickCancel() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Projects"></QuickNav>
|
<QuickNav selected="Projects"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
><fa icon="chevron-left" class="fa-fw"></fa
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
></router-link>
|
></router-link>
|
||||||
Edit Idea
|
[New/Edit] Plan
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -24,43 +24,32 @@
|
|||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Idea Name"
|
placeholder="Project Name"
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
v-model="fullClaim.name"
|
v-model="projectName"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
rows="5"
|
rows="5"
|
||||||
v-model="fullClaim.description"
|
v-model="description"
|
||||||
maxlength="5000"
|
maxlength="500"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
||||||
{{ fullClaim.description?.length }}/5000 max. characters
|
{{ description.length }}/500 max. characters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
|
||||||
v-model="fullClaim.url"
|
|
||||||
placeholder="Website"
|
|
||||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
v-model="includeLocation"
|
v-model="includeLocation"
|
||||||
@click="includeLocation = !includeLocation"
|
@change="includeLocation = true"
|
||||||
/>
|
/>
|
||||||
<label for="includeLocation">Include Location</label>
|
<label for="includeLocation">Include Location</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
||||||
<div class="px-2 py-2">
|
|
||||||
For your security, we recommend you choose a location nearby but not
|
|
||||||
exactly at the place.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<l-map
|
<l-map
|
||||||
ref="map"
|
ref="map"
|
||||||
v-model:zoom="zoom"
|
v-model:zoom="zoom"
|
||||||
@@ -78,7 +67,7 @@
|
|||||||
name="OpenStreetMap"
|
name="OpenStreetMap"
|
||||||
/>
|
/>
|
||||||
<l-marker
|
<l-marker
|
||||||
v-if="latitude && longitude"
|
v-if="latitude || longitude"
|
||||||
:lat-lng="[latitude, longitude]"
|
:lat-lng="[latitude, longitude]"
|
||||||
@click="maybeEraseLatLong()"
|
@click="maybeEraseLatLong()"
|
||||||
/>
|
/>
|
||||||
@@ -142,17 +131,13 @@ export default class NewEditProjectView extends Vue {
|
|||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
description = "";
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
fullClaim: PlanVerifiableCredential = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "PlanAction",
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
}; // this default is only to avoid errors before plan is loaded
|
|
||||||
includeLocation = false;
|
includeLocation = false;
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
projectName = "";
|
||||||
zoom = 2;
|
zoom = 2;
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
@@ -224,12 +209,9 @@ export default class NewEditProjectView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.fullClaim = resp.data.claim;
|
const claim = resp.data.claim;
|
||||||
if (this.fullClaim?.location) {
|
this.projectName = claim.name;
|
||||||
this.includeLocation = true;
|
this.description = claim.description;
|
||||||
this.latitude = this.fullClaim.location.geo.latitude;
|
|
||||||
this.longitude = this.fullClaim.location.geo.longitude;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Got error retrieving that project", error);
|
console.error("Got error retrieving that project", error);
|
||||||
@@ -238,7 +220,13 @@ export default class NewEditProjectView extends Vue {
|
|||||||
|
|
||||||
private async SaveProject(identity: IIdentifier) {
|
private async SaveProject(identity: IIdentifier) {
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim: PlanVerifiableCredential = this.fullClaim;
|
const vcClaim: PlanVerifiableCredential = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "PlanAction",
|
||||||
|
name: this.projectName,
|
||||||
|
description: this.description,
|
||||||
|
identifier: this.projectId || undefined,
|
||||||
|
};
|
||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
vcClaim.identifier = this.projectId;
|
vcClaim.identifier = this.projectId;
|
||||||
}
|
}
|
||||||
@@ -300,20 +288,6 @@ export default class NewEditProjectView extends Vue {
|
|||||||
2000,
|
2000,
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"Got unexpected 'data' inside response from server",
|
|
||||||
resp,
|
|
||||||
);
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Saving Idea",
|
|
||||||
text: "Server did not save the idea. Try again.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let userMessage = "There was an error saving the project.";
|
let userMessage = "There was an error saving the project.";
|
||||||
@@ -321,8 +295,8 @@ export default class NewEditProjectView extends Vue {
|
|||||||
error?: { message?: string };
|
error?: { message?: string };
|
||||||
}>;
|
}>;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
console.log("Got error from server", serverError);
|
|
||||||
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
||||||
|
console.log(serverError);
|
||||||
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
|
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Heading -->
|
||||||
<div class="mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
<!-- Back -->
|
Your Identity
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
</h1>
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Your Identity
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-center py-12">
|
<div class="flex justify-center py-12">
|
||||||
<span />
|
<span />
|
||||||
@@ -88,7 +74,7 @@ export default class NewIdentifierView extends Vue {
|
|||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$router.push({ name: "home" });
|
this.$router.push({ name: "account" });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav selected="Projects"></QuickNav>
|
||||||
<TopMessage />
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div id="ViewBreadcrumb" class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1 class="text-lg text-center font-light relative px-7">
|
||||||
@@ -14,7 +12,7 @@
|
|||||||
>
|
>
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
Idea
|
View Plan
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,7 +35,7 @@
|
|||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
{{ issuer }}
|
{{ issuer }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="timeSince">
|
<div>
|
||||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||||
{{ timeSince }}
|
{{ timeSince }}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,14 +45,8 @@
|
|||||||
:href="getOpenStreetMapUrl()"
|
:href="getOpenStreetMapUrl()"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>Map View
|
>
|
||||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
Map View
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div v-if="url">
|
|
||||||
<fa icon="globe" class="fa-fw text-slate-400"></fa>
|
|
||||||
<a :href="addScheme(url)" target="_blank" class="underline"
|
|
||||||
>{{ domainForWebsite(this.url) }}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,11 +56,8 @@
|
|||||||
<div class="text-sm text-slate-500">
|
<div class="text-sm text-slate-500">
|
||||||
<div v-if="!expanded">
|
<div v-if="!expanded">
|
||||||
{{ truncatedDesc }}
|
{{ truncatedDesc }}
|
||||||
<a
|
<a v-if="description.length >= truncateLength" @click="expandText"
|
||||||
v-if="description.length >= truncateLength"
|
>Read More</a
|
||||||
@click="expandText"
|
|
||||||
class="uppercase text-xs font-semibold text-slate-700"
|
|
||||||
>... Read More</a
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@@ -76,7 +65,7 @@
|
|||||||
<a
|
<a
|
||||||
@click="collapseText"
|
@click="collapseText"
|
||||||
class="uppercase text-xs font-semibold text-slate-700"
|
class="uppercase text-xs font-semibold text-slate-700"
|
||||||
>- Read Less</a
|
>Read Less</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,30 +80,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="activeDid" class="mb-4">
|
<div>
|
||||||
<div class="text-center">
|
<div v-if="activeDid" class="text-center">
|
||||||
<button
|
<button
|
||||||
@click="openOfferDialog({ name: 'you', did: activeDid })"
|
@click="openDialog({ name: 'you', did: activeDid })"
|
||||||
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
|
||||||
>
|
|
||||||
I offer…
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="activeDid">
|
|
||||||
<div class="text-center">
|
|
||||||
<button
|
|
||||||
@click="openGiftDialog({ name: 'you', did: activeDid })"
|
|
||||||
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
class="block w-full text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
I gave…
|
I gave…
|
||||||
</button>
|
</button>
|
||||||
<p class="mt-2 mb-4 text-center">Or, record a contribution from:</p>
|
<p class="mt-2 mb-4 text-center">Or, record a gift from:</p>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="!activeDid" class="mt-2 mb-4">Record a gift from:</p>
|
||||||
|
|
||||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||||
<li @click="openGiftDialog()">
|
<li @click="openDialog()">
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:entityId="null"
|
:entityId="null"
|
||||||
:iconSize="64"
|
:iconSize="64"
|
||||||
@@ -129,7 +108,7 @@
|
|||||||
<li
|
<li
|
||||||
v-for="contact in allContacts"
|
v-for="contact in allContacts"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
@click="openGiftDialog(contact)"
|
@click="openDialog(contact)"
|
||||||
>
|
>
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:entityId="contact.did"
|
:entityId="contact.did"
|
||||||
@@ -155,51 +134,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gifts to & from this -->
|
<!-- Gifts to & from this -->
|
||||||
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4">
|
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
Offered To This Idea
|
Given to this Project
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div v-if="offersToThis.length === 0">
|
<ul class="text-sm border-t border-slate-300">
|
||||||
(None yet. Record one above.)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
|
||||||
<li
|
|
||||||
v-for="offer in offersToThis"
|
|
||||||
:key="offer.id"
|
|
||||||
class="py-1.5 border-b border-slate-300"
|
|
||||||
>
|
|
||||||
<div class="flex justify-between gap-4">
|
|
||||||
<span>
|
|
||||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
|
||||||
{{ didInfo(offer.agentDid, activeDid, allMyDids, allContacts) }}
|
|
||||||
</span>
|
|
||||||
<a @click="onClickLoadClaim(offer.jwtId)">
|
|
||||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
|
||||||
</a>
|
|
||||||
<span v-if="offer.amount">
|
|
||||||
<fa
|
|
||||||
:icon="iconForUnitCode(offer.unit)"
|
|
||||||
class="fa-fw text-slate-400"
|
|
||||||
/>{{ offer.amount }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="offer.objectDescription" class="text-slate-500">
|
|
||||||
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
|
||||||
{{ offer.objectDescription }}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Given To This Idea</h3>
|
|
||||||
|
|
||||||
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
|
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
|
||||||
<li
|
<li
|
||||||
v-for="give in givesToThis"
|
v-for="give in givesToThis"
|
||||||
:key="give.id"
|
:key="give.id"
|
||||||
@@ -210,14 +151,9 @@
|
|||||||
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
</span>
|
</span>
|
||||||
<a @click="onClickLoadClaim(give.jwtId)">
|
<span v-if="give.amount"
|
||||||
<fa icon="circle-info" class="pl-2 pt-1 text-slate-500"></fa>
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
</a>
|
{{ give.amount }}
|
||||||
<span v-if="give.amount">
|
|
||||||
<fa
|
|
||||||
:icon="iconForUnitCode(give.unit)"
|
|
||||||
class="fa-fw text-slate-400"
|
|
||||||
/>{{ give.amount }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="give.description" class="text-slate-500">
|
<div v-if="give.description" class="text-slate-500">
|
||||||
@@ -228,52 +164,42 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid items-start grid-cols-1 gap-4">
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<div
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
v-if="fulfillersToThis.length > 0"
|
…and from this Project
|
||||||
class="bg-slate-100 px-4 py-3 rounded-md"
|
</h3>
|
||||||
>
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
|
||||||
Contributions To This Idea
|
|
||||||
</h3>
|
|
||||||
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
|
||||||
<div class="text-center">
|
|
||||||
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
|
|
||||||
<button
|
|
||||||
@click="onClickLoadProject(plan.handleId)"
|
|
||||||
class="text-blue-500"
|
|
||||||
>
|
|
||||||
{{ plan.name }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
<ul class="text-sm border-t border-slate-300">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<li
|
||||||
Contributions By This Idea
|
v-for="give in givesByThis"
|
||||||
</h3>
|
:key="give.id"
|
||||||
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
class="py-1.5 border-b border-slate-300"
|
||||||
<div class="text-center">
|
>
|
||||||
<button
|
<div class="flex justify-between gap-4">
|
||||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
<span
|
||||||
class="text-blue-500"
|
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||||
>
|
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
||||||
{{ fulfilledByThis.name }}
|
</span>
|
||||||
</button>
|
<span v-if="give.amount"
|
||||||
</div>
|
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
||||||
</div>
|
{{ give.amount }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="give.description" class="text-slate-500">
|
||||||
|
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
||||||
|
{{ give.description }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiftedDialog
|
<GiftedDialog
|
||||||
ref="customGiveDialog"
|
ref="customDialog"
|
||||||
message="Received from"
|
message="Received from"
|
||||||
:projectId="this.projectId"
|
:projectId="this.projectId"
|
||||||
>
|
>
|
||||||
</GiftedDialog>
|
</GiftedDialog>
|
||||||
<OfferDialog ref="customOfferDialog" :projectId="this.projectId">
|
|
||||||
</OfferDialog>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -284,19 +210,14 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
import OfferDialog from "@/components/OfferDialog.vue";
|
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { isGlobalUri } from "@/libs/util";
|
|
||||||
import {
|
import {
|
||||||
didInfo,
|
didInfo,
|
||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
OfferServerRecord,
|
|
||||||
PlanServerRecord,
|
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
@@ -310,7 +231,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { EntityIcon, GiftedDialog, OfferDialog, QuickNav, TopMessage },
|
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ProjectViewView extends Vue {
|
export default class ProjectViewView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -321,19 +242,16 @@ export default class ProjectViewView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
description = "";
|
description = "";
|
||||||
expanded = false;
|
expanded = false;
|
||||||
fulfilledByThis: PlanServerRecord | null = null;
|
|
||||||
fulfillersToThis: Array<PlanServerRecord> = [];
|
|
||||||
givesToThis: Array<GiveServerRecord> = [];
|
givesToThis: Array<GiveServerRecord> = [];
|
||||||
issuer = "";
|
givesByThis: Array<GiveServerRecord> = [];
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
name = "";
|
name = "";
|
||||||
offersToThis: Array<OfferServerRecord> = [];
|
issuer = "";
|
||||||
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
||||||
timeSince = "";
|
timeSince = "";
|
||||||
truncatedDesc = "";
|
truncatedDesc = "";
|
||||||
truncateLength = 40;
|
truncateLength = 40;
|
||||||
url = "";
|
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
@@ -348,12 +266,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
this.LoadProject(identity);
|
||||||
const pathParam = window.location.pathname.substring("/project/".length);
|
|
||||||
if (pathParam) {
|
|
||||||
this.projectId = decodeURIComponent(pathParam);
|
|
||||||
}
|
|
||||||
this.LoadProject(this.projectId, identity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
@@ -363,6 +276,12 @@ export default class ProjectViewView extends Vue {
|
|||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first()) as Account;
|
.first()) as Account;
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error(
|
||||||
|
"Attempted to load project records with no identity available.",
|
||||||
|
);
|
||||||
|
}
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,11 +320,11 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async LoadProject(projectId: string, identity: IIdentifier) {
|
async LoadProject(identity: IIdentifier) {
|
||||||
this.projectId = projectId;
|
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
|
this.apiServer +
|
||||||
|
"/api/claim/byHandle/" +
|
||||||
|
encodeURIComponent(this.projectId);
|
||||||
const headers: RawAxiosRequestHeaders = {
|
const headers: RawAxiosRequestHeaders = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
@@ -429,22 +348,19 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
||||||
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
||||||
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
||||||
this.url = resp.data.claim?.url || "";
|
} else if (resp.status === 404) {
|
||||||
} else {
|
// actually, axios throws an error so we never get here
|
||||||
// actually, axios throws an error on 404 so we probably never get here
|
|
||||||
console.log("Error getting project:", resp);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "There was a problem getting that project. See logs for more info.",
|
text: "That project does not exist.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error("Error retrieving project:", error);
|
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
if (serverError.response?.status === 404) {
|
if (serverError.response?.status === 404) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -466,13 +382,14 @@ export default class ProjectViewView extends Vue {
|
|||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
|
console.error("Error retrieving project:", serverError.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const givesInUrl =
|
const givesInUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/givesForPlans?planIds=" +
|
"/api/v2/report/givesForPlans?planIds=" +
|
||||||
encodeURIComponent(JSON.stringify([projectId]));
|
encodeURIComponent(JSON.stringify([this.projectId]));
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(givesInUrl, { headers });
|
const resp = await this.axios.get(givesInUrl, { headers });
|
||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200 && resp.data.data) {
|
||||||
@@ -505,21 +422,21 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offersToUrl =
|
const givesOutUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/offersToPlans?planIds=" +
|
"/api/v2/report/givesProvidedBy?providerId=" +
|
||||||
encodeURIComponent(JSON.stringify([projectId]));
|
encodeURIComponent(this.projectId);
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(offersToUrl, { headers });
|
const resp = await this.axios.get(givesOutUrl, { headers });
|
||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200 && resp.data.data) {
|
||||||
this.offersToThis = resp.data.data;
|
this.givesByThis = resp.data.data;
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Failed to retrieve offers to this project.",
|
text: "Failed to retrieve gives by this project.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -531,100 +448,15 @@ export default class ProjectViewView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Something went wrong retrieving offers to this project.",
|
text: "Something went wrong retrieving gives by project.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Error retrieving offers to this project:",
|
"Error retrieving gives by this project:",
|
||||||
serverError.message,
|
serverError.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fulfilledByUrl =
|
|
||||||
this.apiServer +
|
|
||||||
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
|
||||||
encodeURIComponent(projectId);
|
|
||||||
try {
|
|
||||||
const resp = await this.axios.get(fulfilledByUrl, { headers });
|
|
||||||
if (resp.status === 200) {
|
|
||||||
this.fulfilledByThis = resp.data.data;
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Failed to retrieve plans fulfilled by this project.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const serverError = error as AxiosError;
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Something went wrong retrieving plans fulfilled by this project.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Error retrieving plans fulfilled by this project:",
|
|
||||||
serverError.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fulfillersToUrl =
|
|
||||||
this.apiServer +
|
|
||||||
"/api/v2/report/planFulfillersToPlan?planHandleId=" +
|
|
||||||
encodeURIComponent(projectId);
|
|
||||||
try {
|
|
||||||
const resp = await this.axios.get(fulfillersToUrl, { headers });
|
|
||||||
if (resp.status === 200) {
|
|
||||||
this.fulfillersToThis = resp.data.data;
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Failed to retrieve plan fulfillers to this project.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const serverError = error as AxiosError;
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Something went wrong retrieving plan fulfillers to this project.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Error retrieving plan fulfillers to this project:",
|
|
||||||
serverError.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle clicking on a project entry found in the list
|
|
||||||
* @param id of the project
|
|
||||||
**/
|
|
||||||
async onClickLoadProject(projectId: string) {
|
|
||||||
localStorage.setItem("projectId", projectId);
|
|
||||||
const route = {
|
|
||||||
path: "/project/" + encodeURIComponent(projectId),
|
|
||||||
};
|
|
||||||
this.$router.push(route);
|
|
||||||
this.LoadProject(projectId, await this.getIdentity(this.activeDid));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenStreetMapUrl() {
|
getOpenStreetMapUrl() {
|
||||||
@@ -641,66 +473,8 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
openGiftDialog(contact: GiverInputInfo) {
|
openDialog(contact: GiverInputInfo) {
|
||||||
(this.$refs.customGiveDialog as GiftedDialog).open(contact);
|
(this.$refs.customDialog as GiftedDialog).open(contact);
|
||||||
}
|
|
||||||
|
|
||||||
openOfferDialog() {
|
|
||||||
(this.$refs.customOfferDialog as OfferDialog).open();
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickLoadClaim(jwtId: string) {
|
|
||||||
const route = {
|
|
||||||
path: "/claim/" + encodeURIComponent(jwtId),
|
|
||||||
};
|
|
||||||
this.$router.push(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
UNIT_CODES: Record<string, Record<string, string>> = {
|
|
||||||
BTC: {
|
|
||||||
name: "Bitcoin",
|
|
||||||
faIcon: "bitcoin-sign",
|
|
||||||
},
|
|
||||||
HUR: {
|
|
||||||
name: "hours",
|
|
||||||
faIcon: "clock",
|
|
||||||
},
|
|
||||||
USD: {
|
|
||||||
name: "US Dollars",
|
|
||||||
faIcon: "dollar",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
iconForUnitCode(unitCode: string) {
|
|
||||||
return this.UNIT_CODES[unitCode]?.faIcon || "question";
|
|
||||||
}
|
|
||||||
|
|
||||||
// return an HTTPS URL if it's not a global URL
|
|
||||||
addScheme(url: string) {
|
|
||||||
if (!isGlobalUri(url)) {
|
|
||||||
return "https://" + url;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// return just the domain for display, if possible
|
|
||||||
domainForWebsite(url: string) {
|
|
||||||
try {
|
|
||||||
const hostname = new URL(url).hostname;
|
|
||||||
if (!hostname) {
|
|
||||||
// happens for non-http URLs
|
|
||||||
return url;
|
|
||||||
} else if (url.endsWith(hostname)) {
|
|
||||||
// it's just the domain
|
|
||||||
return hostname;
|
|
||||||
} else {
|
|
||||||
// there's more, but don't bother displaying the whole thing
|
|
||||||
return hostname + "...";
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
// must not be a valid URL
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Projects"></QuickNav>
|
<QuickNav selected="Projects"></QuickNav>
|
||||||
<TopMessage />
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Your Ideas
|
Your Plans
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Quick Search -->
|
<!-- Quick Search -->
|
||||||
@@ -81,7 +79,6 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
|
||||||
import { ProjectData } from "@/libs/endorserServer";
|
import { ProjectData } from "@/libs/endorserServer";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
@@ -92,7 +89,7 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { InfiniteScroll, QuickNav, EntityIcon, TopMessage },
|
components: { InfiniteScroll, QuickNav, EntityIcon },
|
||||||
})
|
})
|
||||||
export default class ProjectsView extends Vue {
|
export default class ProjectsView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
@@ -125,8 +122,8 @@ export default class ProjectsView extends Vue {
|
|||||||
if (resp.status === 200 || !resp.data.data) {
|
if (resp.status === 200 || !resp.data.data) {
|
||||||
const plans: ProjectData[] = resp.data.data;
|
const plans: ProjectData[] = resp.data.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
const { name, description, handleId, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
this.projects.push({ name, description, handleId, rowid });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Bad server response & data:", resp.status, resp.data);
|
console.log("Bad server response & data:", resp.status, resp.data);
|
||||||
@@ -177,7 +174,7 @@ export default class ProjectsView extends Vue {
|
|||||||
onClickLoadProject(id: string) {
|
onClickLoadProject(id: string) {
|
||||||
localStorage.setItem("projectId", id);
|
localStorage.setItem("projectId", id);
|
||||||
const route = {
|
const route = {
|
||||||
path: "/project/" + encodeURIComponent(id),
|
name: "project",
|
||||||
};
|
};
|
||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,286 +0,0 @@
|
|||||||
<template>
|
|
||||||
<QuickNav />
|
|
||||||
|
|
||||||
<!-- CONTENT -->
|
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
||||||
<!-- Breadcrumb -->
|
|
||||||
<div class="mb-8">
|
|
||||||
<!-- Back -->
|
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Area for Nearby Search
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-2 py-4">
|
|
||||||
This location is only stored on your device. It is used to show you more
|
|
||||||
appropriate projects but is not stored on any servers.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
|
||||||
Click to Choose a Location for Nearby Search
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="storeSearchBox"
|
|
||||||
>
|
|
||||||
Store This Location for Nearby Search
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="searchBox"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="forgetSearchBox"
|
|
||||||
>
|
|
||||||
Delete Stored Location
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="searchBox"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="resetLatLong"
|
|
||||||
>
|
|
||||||
Reset Marker
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="isNewMarkerSet = false"
|
|
||||||
>
|
|
||||||
Erase Marker
|
|
||||||
</button>
|
|
||||||
<div v-if="isNewMarkerSet">
|
|
||||||
Click on the pin to erase it. Click anywhere else to set a different
|
|
||||||
different corner.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="height: 600px; width: 800px">
|
|
||||||
<l-map
|
|
||||||
ref="map"
|
|
||||||
:center="[localCenterLat, localCenterLong]"
|
|
||||||
v-model:zoom="localZoom"
|
|
||||||
@click="setMapPoint"
|
|
||||||
>
|
|
||||||
<l-tile-layer
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
layer-type="base"
|
|
||||||
name="OpenStreetMap"
|
|
||||||
/>
|
|
||||||
<l-marker
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
:lat-lng="[localCenterLat, localCenterLong]"
|
|
||||||
@click="isNewMarkerSet = false"
|
|
||||||
/>
|
|
||||||
<l-rectangle
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
:bounds="[
|
|
||||||
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
|
||||||
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
|
||||||
]"
|
|
||||||
:weight="1"
|
|
||||||
/>
|
|
||||||
</l-map>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { LeafletMouseEvent } from "leaflet";
|
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
import {
|
|
||||||
LMap,
|
|
||||||
LMarker,
|
|
||||||
LRectangle,
|
|
||||||
LTileLayer,
|
|
||||||
} from "@vue-leaflet/vue-leaflet";
|
|
||||||
|
|
||||||
import { db } from "@/db/index";
|
|
||||||
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
|
||||||
|
|
||||||
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
|
||||||
const WORLD_ZOOM = 2;
|
|
||||||
const DEFAULT_ZOOM = 2;
|
|
||||||
|
|
||||||
interface Notification {
|
|
||||||
group: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
QuickNav,
|
|
||||||
LRectangle,
|
|
||||||
LMap,
|
|
||||||
LMarker,
|
|
||||||
LTileLayer,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class DiscoverView extends Vue {
|
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
|
||||||
|
|
||||||
isChoosingSearchBox = false;
|
|
||||||
isNewMarkerSet = false;
|
|
||||||
|
|
||||||
// "local" vars are for the currently selected map box
|
|
||||||
localCenterLat = 0;
|
|
||||||
localCenterLong = 0;
|
|
||||||
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
localZoom = DEFAULT_ZOOM;
|
|
||||||
|
|
||||||
// searchBox reflects what is stored in the database
|
|
||||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
await db.open();
|
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
||||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
|
||||||
this.resetLatLong();
|
|
||||||
}
|
|
||||||
|
|
||||||
setMapPoint(event: LeafletMouseEvent) {
|
|
||||||
if (this.isNewMarkerSet) {
|
|
||||||
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
|
||||||
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
|
||||||
} else {
|
|
||||||
// marker is not set
|
|
||||||
this.localCenterLat = event.latlng.lat;
|
|
||||||
this.localCenterLong = event.latlng.lng;
|
|
||||||
|
|
||||||
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
// Guess at a size for the bounding box.
|
|
||||||
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
|
||||||
const bounds = event.target.boxZoom?._map?.getBounds();
|
|
||||||
if (bounds) {
|
|
||||||
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
|
||||||
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
|
||||||
}
|
|
||||||
this.localLatDiff = latDiff;
|
|
||||||
this.localLongDiff = longDiff;
|
|
||||||
this.isNewMarkerSet = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetLatLong() {
|
|
||||||
if (this.searchBox?.bbox) {
|
|
||||||
const bbox = this.searchBox.bbox;
|
|
||||||
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
|
||||||
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
|
||||||
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
|
||||||
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
|
||||||
this.localZoom = WORLD_ZOOM;
|
|
||||||
this.isNewMarkerSet = true;
|
|
||||||
} else {
|
|
||||||
this.isNewMarkerSet = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async storeSearchBox() {
|
|
||||||
if (this.localCenterLong || this.localCenterLat) {
|
|
||||||
try {
|
|
||||||
const newSearchBox = {
|
|
||||||
name: "Local",
|
|
||||||
bbox: {
|
|
||||||
eastLong: this.localCenterLong + this.localLongDiff,
|
|
||||||
maxLat: this.localCenterLat + this.localLatDiff,
|
|
||||||
minLat: this.localCenterLat - this.localLatDiff,
|
|
||||||
westLong: this.localCenterLong - this.localLongDiff,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
searchBoxes: [newSearchBox],
|
|
||||||
});
|
|
||||||
this.searchBox = newSearchBox;
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "success",
|
|
||||||
title: "Saved",
|
|
||||||
text: "That has been saved in your preferences.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
this.$router.back();
|
|
||||||
} catch (err) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Updating Search Settings",
|
|
||||||
text: "Try going to a different page and then coming back.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Telling user to retry the location search setting because:",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "No Location Selected",
|
|
||||||
text: "Select a location on the map.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async forgetSearchBox() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
searchBoxes: [],
|
|
||||||
});
|
|
||||||
this.searchBox = null;
|
|
||||||
this.localCenterLat = 0;
|
|
||||||
this.localCenterLong = 0;
|
|
||||||
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
this.localZoom = DEFAULT_ZOOM;
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
this.isNewMarkerSet = false;
|
|
||||||
} catch (err) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Updating Search Settings",
|
|
||||||
text: "Try going to a different page and then coming back.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Telling user to retry the location search setting because:",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelSearchBoxSelect() {
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
this.localZoom = WORLD_ZOOM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav selected="Profile"></QuickNav>
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Heading -->
|
<!-- Heading -->
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Seed Backup
|
Seed Backup
|
||||||
|
|||||||
@@ -3,23 +3,10 @@
|
|||||||
id="Content"
|
id="Content"
|
||||||
class="p-6 pb-24 min-h-screen flex flex-col justify-center"
|
class="p-6 pb-24 min-h-screen flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<!-- Breadcrumb -->
|
<!-- Heading -->
|
||||||
<div class="mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
<!-- Back -->
|
Start Here
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
</h1>
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Start Here
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- id used by puppeteer test script -->
|
<!-- id used by puppeteer test script -->
|
||||||
<div id="start-question" class="mt-8">
|
<div id="start-question" class="mt-8">
|
||||||
|
|||||||
@@ -1,29 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Heading -->
|
||||||
<div class="mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
<!-- Back -->
|
Achievements & Statistics
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
</h1>
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Achievements & Statistics
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Here is a view of the activity you can see.
|
Here is a view of the activity you can see.
|
||||||
<ul class="list-disc outside ml-4">
|
<ul class="list-disc list-inside">
|
||||||
<li>Each identity and claim has a unique position.</li>
|
<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. -->
|
<!-- 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>Each will show at their time of appearance relative to all others.</li>
|
||||||
@@ -46,7 +32,7 @@
|
|||||||
{{ worldProperties.animationDurationSeconds }} seconds
|
{{ worldProperties.animationDurationSeconds }} seconds
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="float-right text-blue-600" @click="captureGraphics()">Screenshot</button>
|
<button class="float-right" @click="captureGraphics()">Screenshot</button>
|
||||||
<div id="scene-container" class="h-screen"></div>
|
<div id="scene-container" class="h-screen"></div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<QuickNav />
|
<QuickNav selected="Profile"></QuickNav>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Heading -->
|
||||||
<div class="mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
<!-- Back -->
|
Test
|
||||||
<div class="text-lg text-center font-light relative px-7">
|
</h1>
|
||||||
<h1
|
|
||||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
||||||
@click="$router.back()"
|
|
||||||
>
|
|
||||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Heading -->
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Test
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
|
<h2 class="text-xl font-bold mb-4">Notiwind Alert Test Suite</h2>
|
||||||
@@ -30,14 +16,14 @@
|
|||||||
{
|
{
|
||||||
group: 'alert',
|
group: 'alert',
|
||||||
type: 'toast',
|
type: 'toast',
|
||||||
text: 'I\'m a toast. Without a timeout, I\'m stuck.',
|
text: 'I\'m a toast. Don\'t mind me.',
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="font-bold uppercase bg-slate-900 text-white px-3 py-2 rounded-md mr-2"
|
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
|
||||||
>
|
>
|
||||||
Toast
|
Toast (self-dismiss)
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
/* eslint-env serviceworker */
|
|
||||||
/* global workbox */
|
|
||||||
importScripts(
|
|
||||||
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js",
|
|
||||||
);
|
|
||||||
|
|
||||||
function logConsoleAndDb(message, arg1, arg2) {
|
|
||||||
// in chrome://serviceworker-internals note that the arg1 and arg2 here will show as "[object Object]" in that page but will show as expandable objects in the console
|
|
||||||
console.log(`${new Date().toISOString()} ${message}`, arg1, arg2);
|
|
||||||
if (self.appendDailyLog) {
|
|
||||||
let fullMessage = `${new Date().toISOString()} ${message}`;
|
|
||||||
if (arg1) {
|
|
||||||
fullMessage += `\n${JSON.stringify(arg1)}`;
|
|
||||||
}
|
|
||||||
if (arg2) {
|
|
||||||
fullMessage += `\n${JSON.stringify(arg2)}`;
|
|
||||||
}
|
|
||||||
self.appendDailyLog(fullMessage);
|
|
||||||
} else {
|
|
||||||
// sometimes we get the error: "Uncaught TypeError: self.appendDailyLog is not a function"
|
|
||||||
console.log(
|
|
||||||
"Not logging to DB (often because self.appendDailyLog doesn't exist).",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addEventListener("install", async (event) => {
|
|
||||||
console.log("Service worker got install event. Importing scripts...", event);
|
|
||||||
await importScripts(
|
|
||||||
"safari-notifications.js",
|
|
||||||
"nacl.js",
|
|
||||||
"noble-curves.js",
|
|
||||||
"noble-hashes.js",
|
|
||||||
);
|
|
||||||
// this should now be available
|
|
||||||
logConsoleAndDb("Service worker imported all scripts.");
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("activate", (event) => {
|
|
||||||
logConsoleAndDb("Service worker is activating...", event);
|
|
||||||
// see https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
|
|
||||||
// and https://web.dev/articles/service-worker-lifecycle#clientsclaim
|
|
||||||
event.waitUntil(clients.claim());
|
|
||||||
logConsoleAndDb("Service worker is activated.");
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("push", function (event) {
|
|
||||||
let text = null;
|
|
||||||
if (event.data) {
|
|
||||||
text = event.data.text();
|
|
||||||
}
|
|
||||||
logConsoleAndDb("Service worker received a push event.", text, event);
|
|
||||||
event.waitUntil(
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
let payload;
|
|
||||||
if (text) {
|
|
||||||
try {
|
|
||||||
payload = JSON.parse(text);
|
|
||||||
} catch (e) {
|
|
||||||
// don't use payload since it is not JSON
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a special value that tells the service worker to trigger its daily check.
|
|
||||||
// See https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/c1ed026662e754348a5f91542680bd4f57e5b81e/app.py#L217
|
|
||||||
const DAILY_UPDATE_TITLE = "DAILY_CHECK";
|
|
||||||
|
|
||||||
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
|
|
||||||
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
|
|
||||||
// Make sure it is something other than the DAILY_UPDATE_TITLE.
|
|
||||||
const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
|
|
||||||
|
|
||||||
let title;
|
|
||||||
let message = "Got some empty message.";
|
|
||||||
if (payload && payload.title == DIRECT_PUSH_TITLE) {
|
|
||||||
// skip any search logic and show the message directly
|
|
||||||
title = "Direct Notification";
|
|
||||||
message = payload.message || "No details were provided.";
|
|
||||||
} else {
|
|
||||||
// any other title will run through regular filtering logic
|
|
||||||
if (payload && payload.title === DAILY_UPDATE_TITLE) {
|
|
||||||
title = "Daily Update";
|
|
||||||
} else {
|
|
||||||
title = payload.title || "Update";
|
|
||||||
}
|
|
||||||
message = await self.getNotificationCount();
|
|
||||||
}
|
|
||||||
if (message) {
|
|
||||||
const options = {
|
|
||||||
body: message,
|
|
||||||
icon: payload ? payload.icon : "icon.png",
|
|
||||||
badge: payload ? payload.badge : "badge.png",
|
|
||||||
};
|
|
||||||
await self.registration.showNotification(title, options);
|
|
||||||
logConsoleAndDb("Notified user:", options);
|
|
||||||
} else {
|
|
||||||
logConsoleAndDb("No notification message.");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logConsoleAndDb("Error with push event", event, error);
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
|
||||||
logConsoleAndDb("Service worker got a message...", event);
|
|
||||||
if (event.data && event.data.type === "SEND_LOCAL_DATA") {
|
|
||||||
self.secret = event.data.data;
|
|
||||||
event.ports[0].postMessage({ success: true });
|
|
||||||
}
|
|
||||||
logConsoleAndDb("Service worker posted a message.");
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
|
||||||
logConsoleAndDb("Service worker got fetch event.", event);
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("error", (event) => {
|
|
||||||
logConsoleAndDb("Service worker error", event);
|
|
||||||
console.error("Full Error:", event);
|
|
||||||
console.error("Message:", event.message);
|
|
||||||
console.error("File:", event.filename);
|
|
||||||
console.error("Line:", event.lineno);
|
|
||||||
console.error("Column:", event.colno);
|
|
||||||
console.error("Error Object:", event.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
|
||||||
1051
sw_scripts/nacl.js
@@ -1,567 +0,0 @@
|
|||||||
function bufferFromBase64(base64) {
|
|
||||||
const binaryString = atob(base64);
|
|
||||||
const length = binaryString.length;
|
|
||||||
const bytes = new Uint8Array(length);
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromString(str, encoding = "utf8") {
|
|
||||||
if (encoding === "utf8") {
|
|
||||||
return new TextEncoder().encode(str);
|
|
||||||
} else if (encoding === "base16") {
|
|
||||||
if (str.length % 2 !== 0) {
|
|
||||||
throw new Error("Invalid hex string length.");
|
|
||||||
}
|
|
||||||
let bytes = new Uint8Array(str.length / 2);
|
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
|
||||||
bytes[i / 2] = parseInt(str.substring(i, i + 2), 16);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
} else if (encoding === "base64url") {
|
|
||||||
str = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
||||||
while (str.length % 4) {
|
|
||||||
str += "=";
|
|
||||||
}
|
|
||||||
return new Uint8Array(bufferFromBase64(str));
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unsupported encoding "${encoding}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a Uint8Array to a string with the given encoding.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} byteArray - The Uint8Array to convert.
|
|
||||||
* @param {string} [encoding='utf8'] - The desired encoding ('utf8', 'base16', 'base64url').
|
|
||||||
* @returns {string} - The encoded string.
|
|
||||||
* @throws {Error} - Throws an error if the encoding is unsupported.
|
|
||||||
*/
|
|
||||||
function toString(byteArray, encoding = "utf8") {
|
|
||||||
switch (encoding) {
|
|
||||||
case "utf8":
|
|
||||||
return decodeUTF8(byteArray);
|
|
||||||
case "base16":
|
|
||||||
return toBase16(byteArray);
|
|
||||||
case "base64url":
|
|
||||||
return toBase64Url(byteArray);
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported encoding "${encoding}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode a Uint8Array as a UTF-8 string.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} byteArray
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function decodeUTF8(byteArray) {
|
|
||||||
return new TextDecoder().decode(byteArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a Uint8Array to a base16 (hex) encoded string.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} byteArray
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function toBase16(byteArray) {
|
|
||||||
return Array.from(byteArray)
|
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a Uint8Array to a base64url encoded string.
|
|
||||||
*
|
|
||||||
* @param {Uint8Array} byteArray
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function toBase64Url(byteArray) {
|
|
||||||
let uint8Array = new Uint8Array(byteArray);
|
|
||||||
let binaryString = "";
|
|
||||||
for (let i = 0; i < uint8Array.length; i++) {
|
|
||||||
binaryString += String.fromCharCode(uint8Array[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode to base64
|
|
||||||
let base64 = btoa(binaryString);
|
|
||||||
|
|
||||||
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
const u8a = { toString, fromString };
|
|
||||||
|
|
||||||
function sha256(payload) {
|
|
||||||
const data = typeof payload === "string" ? u8a.fromString(payload) : payload;
|
|
||||||
return nobleHashes.sha256(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function accessToken(identifier) {
|
|
||||||
const did = identifier["did"];
|
|
||||||
const privateKeyHex = identifier["keys"][0]["privateKeyHex"];
|
|
||||||
|
|
||||||
const signer = await SimpleSigner(privateKeyHex);
|
|
||||||
|
|
||||||
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
||||||
const endEpoch = nowEpoch + 60; // add one minute
|
|
||||||
|
|
||||||
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
|
|
||||||
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
|
|
||||||
const jwt = await createJWT(tokenPayload, {
|
|
||||||
alg,
|
|
||||||
issuer: did,
|
|
||||||
signer,
|
|
||||||
});
|
|
||||||
return jwt;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createJWT(payload, options, header = {}) {
|
|
||||||
const { issuer, signer, alg, expiresIn, canonicalize } = options;
|
|
||||||
|
|
||||||
if (!signer)
|
|
||||||
throw new Error(
|
|
||||||
"missing_signer: No Signer functionality has been configured",
|
|
||||||
);
|
|
||||||
if (!issuer)
|
|
||||||
throw new Error("missing_issuer: No issuing DID has been configured");
|
|
||||||
if (!header.typ) header.typ = "JWT";
|
|
||||||
if (!header.alg) header.alg = alg;
|
|
||||||
|
|
||||||
const timestamps = {
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
exp: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (expiresIn) {
|
|
||||||
if (typeof expiresIn === "number") {
|
|
||||||
timestamps.exp = (payload.nbf || timestamps.iat) + Math.floor(expiresIn);
|
|
||||||
} else {
|
|
||||||
throw new Error("invalid_argument: JWT expiresIn is not a number");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPayload = { ...timestamps, ...payload, iss: issuer };
|
|
||||||
return createJWS(fullPayload, signer, header, { canonicalize });
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultAlg = "ES256K";
|
|
||||||
|
|
||||||
async function createJWS(payload, signer, header = {}, options = {}) {
|
|
||||||
if (!header.alg) header.alg = defaultAlg;
|
|
||||||
const encodedPayload =
|
|
||||||
typeof payload === "string"
|
|
||||||
? payload
|
|
||||||
: encodeSection(payload, options.canonicalize);
|
|
||||||
const signingInput = [
|
|
||||||
encodeSection(header, options.canonicalize),
|
|
||||||
encodedPayload,
|
|
||||||
].join(".");
|
|
||||||
|
|
||||||
const jwtSigner = ES256KSignerAlg(false);
|
|
||||||
const signature = await jwtSigner(signingInput, signer);
|
|
||||||
|
|
||||||
// JWS Compact Serialization
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc7515#section-7.1
|
|
||||||
return [signingInput, signature].join(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
function canonicalizeData(object) {
|
|
||||||
if (typeof object === "number" && isNaN(object)) {
|
|
||||||
throw new Error("NaN is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof object === "number" && !isFinite(object)) {
|
|
||||||
throw new Error("Infinity is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (object === null || typeof object !== "object") {
|
|
||||||
return JSON.stringify(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (object.toJSON instanceof Function) {
|
|
||||||
return serialize(object.toJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(object)) {
|
|
||||||
const values = object.reduce((t, cv, ci) => {
|
|
||||||
const comma = ci === 0 ? "" : ",";
|
|
||||||
const value = cv === undefined || typeof cv === "symbol" ? null : cv;
|
|
||||||
return `${t}${comma}${serialize(value)}`;
|
|
||||||
}, "");
|
|
||||||
return `[${values}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = Object.keys(object)
|
|
||||||
.sort()
|
|
||||||
.reduce((t, cv) => {
|
|
||||||
if (object[cv] === undefined || typeof object[cv] === "symbol") {
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
const comma = t.length === 0 ? "" : ",";
|
|
||||||
return `${t}${comma}${serialize(cv)}:${serialize(object[cv])}`;
|
|
||||||
}, "");
|
|
||||||
return `{${values}}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeSection(data, shouldCanonicalize = false) {
|
|
||||||
if (shouldCanonicalize) {
|
|
||||||
return encodeBase64url(canonicalizeData(data));
|
|
||||||
} else {
|
|
||||||
return encodeBase64url(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeBase64url(s) {
|
|
||||||
return bytesToBase64url(u8a.fromString(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
function instanceOfEcdsaSignature(object) {
|
|
||||||
return typeof object === "object" && "r" in object && "s" in object;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ES256KSignerAlg(recoverable) {
|
|
||||||
return async function sign(payload, signer) {
|
|
||||||
const signature = await signer(payload);
|
|
||||||
if (instanceOfEcdsaSignature(signature)) {
|
|
||||||
return toJose(signature, recoverable);
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
recoverable &&
|
|
||||||
typeof fromJose(signature).recoveryParam === "undefined"
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`not_supported: ES256K-R not supported when signer doesn't provide a recovery param`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function leftpad(data, size = 64) {
|
|
||||||
if (data.length === size) return data;
|
|
||||||
return "0".repeat(size - data.length) + data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function SimpleSigner(hexPrivateKey) {
|
|
||||||
const signer = await ES256KSigner(hexToBytes(hexPrivateKey), true);
|
|
||||||
return async (data) => {
|
|
||||||
const signature = await signer(data);
|
|
||||||
return fromJose(signature);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToBytes(s, minLength) {
|
|
||||||
let input = s.startsWith("0x") ? s.substring(2) : s;
|
|
||||||
|
|
||||||
if (input.length % 2 !== 0) {
|
|
||||||
input = `0${input}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minLength) {
|
|
||||||
const paddedLength = Math.max(input.length, minLength * 2);
|
|
||||||
input = input.padStart(paddedLength, "00");
|
|
||||||
}
|
|
||||||
|
|
||||||
return u8a.fromString(input.toLowerCase(), "base16");
|
|
||||||
}
|
|
||||||
|
|
||||||
function ES256KSigner(privateKey, recoverable = false) {
|
|
||||||
const privateKeyBytes = privateKey;
|
|
||||||
if (privateKeyBytes.length !== 32) {
|
|
||||||
throw new Error(
|
|
||||||
`bad_key: Invalid private key format. Expecting 32 bytes, but got ${privateKeyBytes.length}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return async function (data) {
|
|
||||||
const signature = nobleCurves.secp256k1.sign(sha256(data), privateKeyBytes);
|
|
||||||
return toJose(
|
|
||||||
{
|
|
||||||
r: leftpad(signature.r.toString(16)),
|
|
||||||
s: leftpad(signature.s.toString(16)),
|
|
||||||
recoveryParam: signature.recovery,
|
|
||||||
},
|
|
||||||
recoverable,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toJose(signature, recoverable) {
|
|
||||||
const { r, s, recoveryParam } = signature;
|
|
||||||
const jose = new Uint8Array(recoverable ? 65 : 64);
|
|
||||||
jose.set(u8a.fromString(r, "base16"), 0);
|
|
||||||
jose.set(u8a.fromString(s, "base16"), 32);
|
|
||||||
|
|
||||||
if (recoverable) {
|
|
||||||
if (typeof recoveryParam === "undefined") {
|
|
||||||
throw new Error("Signer did not return a recoveryParam");
|
|
||||||
}
|
|
||||||
jose[64] = recoveryParam;
|
|
||||||
}
|
|
||||||
return bytesToBase64url(jose);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bytesToBase64url(b) {
|
|
||||||
return u8a.toString(b, "base64url");
|
|
||||||
}
|
|
||||||
|
|
||||||
function base64ToBytes(s) {
|
|
||||||
const inputBase64Url = s
|
|
||||||
.replace(/\+/g, "-")
|
|
||||||
.replace(/\//g, "_")
|
|
||||||
.replace(/=/g, "");
|
|
||||||
return u8a.fromString(inputBase64Url, "base64url");
|
|
||||||
}
|
|
||||||
|
|
||||||
function bytesToHex(b) {
|
|
||||||
return u8a.toString(b, "base16");
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromJose(signature) {
|
|
||||||
const signatureBytes = base64ToBytes(signature);
|
|
||||||
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
|
||||||
throw new TypeError(
|
|
||||||
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const r = bytesToHex(signatureBytes.slice(0, 32));
|
|
||||||
const s = bytesToHex(signatureBytes.slice(32, 64));
|
|
||||||
const recoveryParam =
|
|
||||||
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
|
|
||||||
|
|
||||||
return { r, s, recoveryParam };
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateBase64(s) {
|
|
||||||
if (
|
|
||||||
!/^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test(
|
|
||||||
s,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new TypeError("invalid encoding");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeBase64(s) {
|
|
||||||
validateBase64(s);
|
|
||||||
var i,
|
|
||||||
d = atob(s),
|
|
||||||
b = new Uint8Array(d.length);
|
|
||||||
for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i);
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSettingById(id) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let openRequest = indexedDB.open("TimeSafari");
|
|
||||||
|
|
||||||
openRequest.onupgradeneeded = (event) => {
|
|
||||||
// Handle database setup if necessary
|
|
||||||
let db = event.target.result;
|
|
||||||
if (!db.objectStoreNames.contains("settings")) {
|
|
||||||
db.createObjectStore("settings", { keyPath: "id" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
openRequest.onsuccess = (event) => {
|
|
||||||
let db = event.target.result;
|
|
||||||
let transaction = db.transaction("settings", "readonly");
|
|
||||||
let objectStore = transaction.objectStore("settings");
|
|
||||||
let getRequest = objectStore.get(id);
|
|
||||||
|
|
||||||
getRequest.onsuccess = () => resolve(getRequest.result);
|
|
||||||
getRequest.onerror = () => reject(getRequest.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
openRequest.onerror = () => reject(openRequest.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setMostRecentNotified(id) {
|
|
||||||
try {
|
|
||||||
const db = await openIndexedDB("TimeSafari");
|
|
||||||
const transaction = db.transaction("settings", "readwrite");
|
|
||||||
const store = transaction.objectStore("settings");
|
|
||||||
const data = await getRecord(store, 1);
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
data["lastNotifiedClaimId"] = id;
|
|
||||||
await updateRecord(store, data);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"safari-notifications setMostRecentNotified IndexedDB settings record not found",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.oncomplete = () => db.close();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"safari-notifications setMostRecentNotified IndexedDB error",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function appendDailyLog(message) {
|
|
||||||
try {
|
|
||||||
const db = await openIndexedDB("TimeSafari");
|
|
||||||
const transaction = db.transaction("logs", "readwrite");
|
|
||||||
const store = transaction.objectStore("logs");
|
|
||||||
// only keep one day's worth of logs
|
|
||||||
const todayKey = new Date().toDateString();
|
|
||||||
const previous = await getRecord(store, todayKey);
|
|
||||||
if (!previous) {
|
|
||||||
await store.clear(); // clear out everything previous when this is today's first log
|
|
||||||
}
|
|
||||||
let fullMessage = (previous && previous.message) || "";
|
|
||||||
if (fullMessage) {
|
|
||||||
fullMessage += "\n";
|
|
||||||
}
|
|
||||||
fullMessage += message;
|
|
||||||
await updateRecord(store, { date: todayKey, message: fullMessage });
|
|
||||||
transaction.oncomplete = () => db.close();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("safari-notifications logMessage IndexedDB error", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openIndexedDB(dbName) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open(dbName);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRecord(store, key) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = store.get(key);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that this assumes there is only one record in the store.
|
|
||||||
function updateRecord(store, data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = store.put(data);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAllAccounts() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const openRequest = indexedDB.open("TimeSafariAccounts");
|
|
||||||
|
|
||||||
openRequest.onupgradeneeded = function (event) {
|
|
||||||
const db = event.target.result;
|
|
||||||
if (!db.objectStoreNames.contains("accounts")) {
|
|
||||||
db.createObjectStore("accounts", { keyPath: "id" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
openRequest.onsuccess = function (event) {
|
|
||||||
const db = event.target.result;
|
|
||||||
const transaction = db.transaction("accounts", "readonly");
|
|
||||||
const objectStore = transaction.objectStore("accounts");
|
|
||||||
const getAllRequest = objectStore.getAll();
|
|
||||||
|
|
||||||
getAllRequest.onsuccess = function () {
|
|
||||||
resolve(getAllRequest.result);
|
|
||||||
};
|
|
||||||
getAllRequest.onerror = function () {
|
|
||||||
reject(getAllRequest.error);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
openRequest.onerror = function () {
|
|
||||||
reject(openRequest.error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNotificationCount() {
|
|
||||||
let accounts = [];
|
|
||||||
let result = null;
|
|
||||||
// 1 is our master settings ID; see MASTER_SETTINGS_KEY
|
|
||||||
const settings = await getSettingById(1);
|
|
||||||
let lastNotifiedClaimId = null;
|
|
||||||
if ("lastNotifiedClaimId" in settings) {
|
|
||||||
lastNotifiedClaimId = settings["lastNotifiedClaimId"];
|
|
||||||
}
|
|
||||||
const activeDid = settings["activeDid"];
|
|
||||||
accounts = await fetchAllAccounts();
|
|
||||||
let activeAccount = null;
|
|
||||||
for (let i = 0; i < accounts.length; i++) {
|
|
||||||
if (accounts[i]["did"] == activeDid) {
|
|
||||||
activeAccount = accounts[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
};
|
|
||||||
|
|
||||||
const identity = activeAccount && activeAccount["identity"];
|
|
||||||
if (identity && "secret" in self) {
|
|
||||||
const secret = self.secret;
|
|
||||||
const secretUint8Array = self.decodeBase64(secret);
|
|
||||||
const messageWithNonceAsUint8Array = self.decodeBase64(identity);
|
|
||||||
const nonce = messageWithNonceAsUint8Array.slice(0, 24);
|
|
||||||
const message = messageWithNonceAsUint8Array.slice(24, identity.length);
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
|
||||||
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
|
|
||||||
const msg = decoder.decode(decrypted);
|
|
||||||
const identifier = JSON.parse(JSON.parse(msg));
|
|
||||||
|
|
||||||
headers["Authorization"] = "Bearer " + (await accessToken(identifier));
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
settings["apiServer"] + "/api/v2/report/claims",
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: headers,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (response.status == 200) {
|
|
||||||
const json = await response.json();
|
|
||||||
const claims = json["data"];
|
|
||||||
let newClaims = 0;
|
|
||||||
for (let i = 0; i < claims.length; i++) {
|
|
||||||
const claim = claims[i];
|
|
||||||
if (claim["id"] === lastNotifiedClaimId) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
newClaims++;
|
|
||||||
}
|
|
||||||
if (newClaims > 0) {
|
|
||||||
result = `There are ${newClaims} new activities on Time Safari`;
|
|
||||||
}
|
|
||||||
const most_recent_notified = claims[0]["id"];
|
|
||||||
await setMostRecentNotified(most_recent_notified);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"safari-notifications getNotificationsCount got a bad response status when fetching claims",
|
|
||||||
response.status,
|
|
||||||
response,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.appendDailyLog = appendDailyLog;
|
|
||||||
self.getNotificationCount = getNotificationCount;
|
|
||||||
self.decodeBase64 = decodeBase64;
|
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
const { defineConfig } = require("@vue/cli-service");
|
const { defineConfig } = require("@vue/cli-service");
|
||||||
const { gitDescribeSync } = require("git-describe");
|
|
||||||
|
|
||||||
process.env.VUE_APP_GIT_HASH = gitDescribeSync().hash;
|
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
transpileDependencies: true,
|
transpileDependencies: true,
|
||||||
configureWebpack: {
|
configureWebpack: {
|
||||||
@@ -15,9 +11,5 @@ module.exports = defineConfig({
|
|||||||
iconPaths: {
|
iconPaths: {
|
||||||
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
faviconSVG: "img/icons/safari-pinned-tab.svg",
|
||||||
},
|
},
|
||||||
workboxPluginMode: "InjectManifest",
|
|
||||||
workboxOptions: {
|
|
||||||
swSrc: "./sw_scripts/additional-scripts.js",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
37
web-push.md
@@ -29,8 +29,8 @@ from the SERVICE.
|
|||||||
The SERVICE will provide context and obtain explicit permission before prompting
|
The SERVICE will provide context and obtain explicit permission before prompting
|
||||||
for notification permission:
|
for notification permission:
|
||||||
|
|
||||||
In order to provide this context and explicit permission, a two-step opt-in process
|
In order to provide this context and explict permission a two-step opt-in process
|
||||||
first presents the user with a pre-permission dialog box that explains
|
where the user is first presented with a pre-permission dialog box that explains
|
||||||
what the notifications are for and why they are useful. This may help reduce the
|
what the notifications are for and why they are useful. This may help reduce the
|
||||||
possibility of users clicking "don't allow".
|
possibility of users clicking "don't allow".
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ The `sw.js` file contains the logic for what a service worker should do.
|
|||||||
It executes in a separate thread of execution from the web page but provides a
|
It executes in a separate thread of execution from the web page but provides a
|
||||||
means of communicating between itself and the web page via messages.
|
means of communicating between itself and the web page via messages.
|
||||||
|
|
||||||
Note that there is a scope that can specify what network requests it may
|
Note that there is a scope can specify what network requests it may
|
||||||
intercept.
|
intercept.
|
||||||
|
|
||||||
The Vue project already has its own service worker but it is possible to
|
The Vue project already has its own service worker but it is possible to
|
||||||
@@ -389,33 +389,4 @@ Simply tap on the Mute Notifications toggle switch in `AccountViewView` to immed
|
|||||||
While notifications are turned on, the user can tap on the App Notifications toggle switch in `AccountViewView` to trigger the Turn Off Notifications Dialog. User is given the following choices:
|
While notifications are turned on, the user can tap on the App Notifications toggle switch in `AccountViewView` to trigger the Turn Off Notifications Dialog. User is given the following choices:
|
||||||
|
|
||||||
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
|
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
|
||||||
- "Leave it On" to make no changes and dismiss the dialog.
|
- "Leave it On" to make no changes and dismiss the dialog.
|
||||||
|
|
||||||
# NOTIFICATION STATES
|
|
||||||
|
|
||||||
* Unpermissioned. Push server cannot send notifications to the user because it does not have permission.
|
|
||||||
This may be the same as when the user gave permission in the past but has since revoked it at the OS or browser
|
|
||||||
level, outside the app. (User can change to Permissioned when the user gives permission.)
|
|
||||||
* Permissioned. (User can change to Unpermissioned via the OS or browser settings.)
|
|
||||||
* Active. (User can change to Muted when the user mutes notifications.)
|
|
||||||
* Muted. (User can change to Active when the user toggles it.)
|
|
||||||
(Turning mute off automatically after some amount of time is not planned in version 1.)
|
|
||||||
|
|
||||||
|
|
||||||
# TROUBLESHOOTING
|
|
||||||
|
|
||||||
## Desktop
|
|
||||||
|
|
||||||
#### Firefox
|
|
||||||
|
|
||||||
Go to `about:debugging` and click on `Inspect` for the service worker.
|
|
||||||
|
|
||||||
#### Chrome
|
|
||||||
|
|
||||||
Go to `chrome://inspect/#service-workers` and click on `Inspect` for the service worker.
|
|
||||||
|
|
||||||
## Mobile
|
|
||||||
|
|
||||||
#### Android
|
|
||||||
|
|
||||||
#### iOS
|
|
||||||