Compare commits
1 Commits
plan-loc
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eb0354b2c |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,16 +1,13 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
signature.bin
|
|
||||||
*.pem
|
|
||||||
verified.txt
|
|
||||||
|
|
||||||
*~
|
*~
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
||||||
# Log filesopenssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
|
# Log files
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
Prerequisites:
|
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
|
||||||
openssl ec -in private.pem -pubout -out public.pem
|
|
||||||
|
|
||||||
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
|
||||||
|
|
||||||
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')
|
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
|
||||||
|
|
||||||
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
|
|
||||||
|
|
||||||
# Read binary signature from file and encode it to Base64 URL-Safe format
|
|
||||||
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
|
||||||
|
|
||||||
# Construct the JWT
|
|
||||||
jwt="$signing_input.$signature_b64"
|
|
||||||
|
|
||||||
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
|
|
||||||
|
|
||||||
|
|
||||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -61,7 +61,6 @@
|
|||||||
"@types/three": "^0.152.1",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-pwa": "~5.0.8",
|
"@vue/cli-plugin-pwa": "~5.0.8",
|
||||||
@@ -75,7 +74,6 @@
|
|||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"leaflet": "^1.9.4",
|
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
@@ -9641,24 +9639,6 @@
|
|||||||
"uint8arrays": "^3.0.0"
|
"uint8arrays": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue-leaflet/vue-leaflet": {
|
|
||||||
"version": "0.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.10.1.tgz",
|
|
||||||
"integrity": "sha512-RNEDk8TbnwrJl8ujdbKgZRFygLCxd0aBcWLQ05q/pGv4+d0jamE3KXQgQBqGAteE1mbQsk3xoNcqqUgaIGfWVg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"vue": "^3.2.25"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/leaflet": "^1.5.7",
|
|
||||||
"leaflet": "^1.6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/leaflet": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz",
|
||||||
@@ -19087,12 +19067,6 @@
|
|||||||
"launch-editor": "^2.6.0"
|
"launch-editor": "^2.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/leaflet": {
|
|
||||||
"version": "1.9.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
|
||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
|
||||||
|
|||||||
@@ -61,7 +61,6 @@
|
|||||||
"@types/three": "^0.152.1",
|
"@types/three": "^0.152.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"@vue/cli-plugin-pwa": "~5.0.8",
|
"@vue/cli-plugin-pwa": "~5.0.8",
|
||||||
@@ -75,7 +74,6 @@
|
|||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
"eslint-plugin-prettier": "^5.0.0-alpha.1",
|
||||||
"eslint-plugin-vue": "^9.15.1",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"leaflet": "^1.9.4",
|
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
|
|
||||||
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
|
- .2 bug - on contacts view, click on "to" & "from" and nothing happens
|
||||||
- 01 add a location for a project via map pin :
|
- 01 add a location for a project via map pin :
|
||||||
- add with a "location" field containing this: { "geo":{ "@type":"GeoCoordinates", "latitude":40.883944, "longitude":-111.884787 } }
|
- add with a "location" field containing this: { "geo":{ "@type":"GeoCoordinates", "latitude":40.883944, "longitude":-111.884787 } }
|
||||||
@@ -16,10 +15,10 @@ tasks:
|
|||||||
- 08 Scan QR code to import into contacts assignee:matthew
|
- 08 Scan QR code to import into contacts assignee:matthew
|
||||||
- SEE: https://github.com/gruhn/vue-qrcode-reader
|
- SEE: https://github.com/gruhn/vue-qrcode-reader
|
||||||
|
|
||||||
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
|
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew
|
||||||
|
|
||||||
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s) assignee-group:ui
|
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
||||||
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui
|
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose
|
||||||
|
|
||||||
- Home Feed & Quick Give screen :
|
- Home Feed & Quick Give screen :
|
||||||
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
- 01 save the feed-viewed status in settings storage ("afterQuery")
|
||||||
@@ -33,10 +32,10 @@ tasks:
|
|||||||
- .5 add project ID to the URL, to make a project publicly-accessible
|
- .5 add project ID to the URL, to make a project publicly-accessible
|
||||||
- .5 remove edit from project page for projects owned by others
|
- .5 remove edit from project page for projects owned by others
|
||||||
- .5 fix where user 0 sees no txns from user 1 on contacts page but sees them on list page
|
- .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 assignee-group:ui
|
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist
|
||||||
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui
|
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose
|
||||||
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
|
||||||
- .2 move 'switch identity' to the advanced section assignee-group:ui
|
- .2 move 'switch identity' to the advanced section
|
||||||
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
|
- .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"
|
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
|
||||||
|
|
||||||
@@ -45,12 +44,10 @@ tasks:
|
|||||||
- 04 allow user to download claims, mine + ones I can see about me from others
|
- 04 allow user to download claims, mine + ones I can see about me from others
|
||||||
- .5 change the derivation path, and regenerate test IDs
|
- .5 change the derivation path, and regenerate test IDs
|
||||||
- 02 allow user to create new DIDs from the same seed phrase (ie. increment derivation path)
|
- 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 assignee-group:ui
|
- .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
|
||||||
- .5 Do we want to combine first name & last name?
|
- .5 Do we want to combine first name & last name?
|
||||||
- .2 Show a warning if both giver and recipient are the same (but still allow?) assignee-group:ui
|
- .2 Show a warning if both giver and recipient are the same (but still allow?)
|
||||||
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
|
||||||
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
|
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
@@ -68,6 +65,7 @@ tasks:
|
|||||||
- 08 thorough testing for errors & edge cases
|
- 08 thorough testing for errors & edge cases
|
||||||
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
||||||
- Add disclaimers.
|
- Add disclaimers.
|
||||||
|
- Rename DB to TimeSafari.
|
||||||
- Switch default server to the public server.
|
- Switch default server to the public server.
|
||||||
- Deploy to a server.
|
- Deploy to a server.
|
||||||
- Ensure public server has limits that work for group adoption.
|
- Ensure public server has limits that work for group adoption.
|
||||||
|
|||||||
28
src/App.vue
28
src/App.vue
@@ -157,7 +157,7 @@
|
|||||||
>
|
>
|
||||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
<p class="text-lg mb-4">
|
<p class="text-lg mb-4">
|
||||||
Would you like to turn on notifications for this app?
|
Would you like to <b>turn on</b> notifications for this app?
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -181,6 +181,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="notification.type === 'notification-mute'"
|
||||||
|
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
|
<p class="text-lg mb-4">
|
||||||
|
Would you like to <b>turn off</b> notifications for this app?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
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
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="close(notification.id)"
|
||||||
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
Keep it On
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Notification>
|
</Notification>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export enum AppString {
|
|||||||
APP_NAME = "Kick-Start with Time",
|
APP_NAME = "Kick-Start with Time",
|
||||||
|
|
||||||
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.endorser.ch:8000",
|
||||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||||
|
|
||||||
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ const router = createRouter({
|
|||||||
|
|
||||||
const errorHandler = (error, to, from) => {
|
const errorHandler = (error, to, from) => {
|
||||||
// Handle the error here
|
// Handle the error here
|
||||||
console.error("Caught in top level error handler:", error, to, from);
|
console.error(error, to, from);
|
||||||
|
console.log("XXXXX");
|
||||||
|
|
||||||
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,7 +123,9 @@
|
|||||||
>
|
>
|
||||||
Switch Identity / No Identity
|
Switch Identity / No Identity
|
||||||
</router-link>
|
</router-link>
|
||||||
<button
|
<label
|
||||||
|
for="toggleNotifications"
|
||||||
|
class="flex items-center cursor-pointer mt-4 mb-8"
|
||||||
@click="
|
@click="
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -133,10 +135,21 @@
|
|||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="block w-full text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-8"
|
|
||||||
>
|
>
|
||||||
Turn on Notifications
|
<!-- toggle -->
|
||||||
</button>
|
<div class="relative">
|
||||||
|
<!-- 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 -->
|
||||||
|
<div class="ml-2">App Notifications</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
:dotsOptions="{ type: 'square' }"
|
:dotsOptions="{ type: 'square' }"
|
||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
/>
|
/>
|
||||||
|
<AlertMessage
|
||||||
|
:alertTitle="alertTitle"
|
||||||
|
:alertMessage="alertMessage"
|
||||||
|
></AlertMessage>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -28,15 +32,18 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { SimpleSigner } from "@/libs/crypto";
|
import { SimpleSigner } from "@/libs/crypto";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
import QuickNav from "@/components/QuickNav";
|
import QuickNav from "@/components/QuickNav";
|
||||||
import { Account } from "@/db/tables/accounts";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const Buffer = require("buffer/").Buffer;
|
const Buffer = require("buffer/").Buffer;
|
||||||
|
alertTitle = "";
|
||||||
|
alertMessage = "";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QRCodeVue3,
|
QRCodeVue3,
|
||||||
|
AlertMessage,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -48,10 +55,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
public async getIdentity(activeDid) {
|
public async getIdentity(activeDid) {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account: Account | undefined = R.find(
|
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||||
(acc) => acc.did === activeDid,
|
|
||||||
accounts,
|
|
||||||
);
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
@@ -62,6 +66,15 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getHeaders(identity) {
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
};
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
|||||||
@@ -39,40 +39,6 @@
|
|||||||
{{ description.length }}/500 max. characters
|
{{ description.length }}/500 max. characters
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mb-4">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="mr-2"
|
|
||||||
v-model="includeLocation"
|
|
||||||
@change="includeLocation = true"
|
|
||||||
/>
|
|
||||||
<label for="includeLocation">Include Location</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
|
||||||
<l-map
|
|
||||||
ref="map"
|
|
||||||
v-model:zoom="zoom"
|
|
||||||
:center="[0, 0]"
|
|
||||||
@click="
|
|
||||||
(event) => {
|
|
||||||
latitude = event.latlng.lat;
|
|
||||||
longitude = event.latlng.lng;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<l-tile-layer
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
layer-type="base"
|
|
||||||
name="OpenStreetMap"
|
|
||||||
/>
|
|
||||||
<l-marker
|
|
||||||
v-if="latitude || longitude"
|
|
||||||
:lat-lng="[latitude, longitude]"
|
|
||||||
@click="maybeEraseLatLong()"
|
|
||||||
/>
|
|
||||||
</l-map>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<button
|
<button
|
||||||
:disabled="isHiddenSave"
|
:disabled="isHiddenSave"
|
||||||
@@ -105,11 +71,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db";
|
import { accountsDB, db } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
@@ -119,21 +83,17 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import AlertMessage from "@/components/AlertMessage";
|
import AlertMessage from "@/components/AlertMessage";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { AlertMessage, LMap, LMarker, LTileLayer },
|
components: { AlertMessage },
|
||||||
})
|
})
|
||||||
export default class NewEditProjectView extends Vue {
|
export default class NewEditProjectView extends Vue {
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
alertTitle = "";
|
|
||||||
alertMessage = "";
|
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
projectName = "";
|
||||||
description = "";
|
description = "";
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
includeLocation = false;
|
|
||||||
latitude = 0;
|
|
||||||
longitude = 0;
|
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
projectName = "";
|
alertTitle = "";
|
||||||
zoom = 2;
|
alertMessage = "";
|
||||||
|
|
||||||
async beforeCreate() {
|
async beforeCreate() {
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
@@ -225,15 +185,6 @@ export default class NewEditProjectView extends Vue {
|
|||||||
if (this.projectId) {
|
if (this.projectId) {
|
||||||
vcClaim.identifier = this.projectId;
|
vcClaim.identifier = this.projectId;
|
||||||
}
|
}
|
||||||
if (this.includeLocation) {
|
|
||||||
vcClaim.location = {
|
|
||||||
geo: {
|
|
||||||
"@type": "GeoCoordinates",
|
|
||||||
latitude: this.latitude,
|
|
||||||
longitude: this.longitude,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
@@ -348,14 +299,6 @@ export default class NewEditProjectView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public maybeEraseLatLong() {
|
|
||||||
if (window.confirm("Are you sure you don't want to mark a location?")) {
|
|
||||||
this.latitude = 0;
|
|
||||||
this.longitude = 0;
|
|
||||||
this.includeLocation = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onCancelClick() {
|
public onCancelClick() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
@click="onClickDerive()"
|
@click="onClickDerive()"
|
||||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mt-2"
|
||||||
>
|
>
|
||||||
Derive New Address from Seed Imported Previously
|
Derive from Existing Account
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
316
web-push.md
316
web-push.md
@@ -1,316 +0,0 @@
|
|||||||
Web Push notifications is a web browser messaging protocol defined by the W3C.
|
|
||||||
|
|
||||||
Discussions of this interesting technology are clouded because of a
|
|
||||||
terminological morass.
|
|
||||||
|
|
||||||
To understand how Web Push operates, we need to observe that are three (and
|
|
||||||
potentially four) parties involved. These are:
|
|
||||||
|
|
||||||
1) The user's web browser. Let's call that BROWSER
|
|
||||||
2) The Web Push Service Provider which is operated by the organization
|
|
||||||
controlling the web browser's source code. Here named PROVIDER. An example of a
|
|
||||||
PROVIDER is FCM (Firebase Cloud Messaging) which is owned by Google.
|
|
||||||
3) The Web Application that a user is visiting from their web browser. Let's
|
|
||||||
call this the SERVICE (short for Web Push application service)
|
|
||||||
4) A Custom Web Push Intermediary Service, either third party or self-hosted.
|
|
||||||
Called INTERMEDIARY here. FCM also may fit in this category if the SERVICE
|
|
||||||
has an API key from FCM.]
|
|
||||||
|
|
||||||
The workflow works like this:
|
|
||||||
|
|
||||||
BROWSER visits a website which hosts a SERVICE.
|
|
||||||
|
|
||||||
The SERVICE asks BROWSER for its permission to subscribe to messages coming
|
|
||||||
from the SERVICE.
|
|
||||||
|
|
||||||
The SERVICE will provide context and obtain explicit permission before prompting
|
|
||||||
for notification permission:
|
|
||||||
|
|
||||||
In order to provide this context and explict permission a two-step opt-in process
|
|
||||||
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
|
|
||||||
possibility of users clicking "don't allow".
|
|
||||||
|
|
||||||
Now, to explain what happens in Typescript, we can activate a browser's
|
|
||||||
permission dialogue in this manner:
|
|
||||||
|
|
||||||
```
|
|
||||||
function askPermission(): Promise<NotificationPermission> {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
const permissionResult = Notification.requestPermission(function(result) {
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (permissionResult) {
|
|
||||||
permissionResult.then(resolve, reject);
|
|
||||||
}
|
|
||||||
}).then(function(permissionResult) {
|
|
||||||
if (permissionResult !== 'granted') {
|
|
||||||
throw new Error("We weren't granted permission.");
|
|
||||||
}
|
|
||||||
return permissionResult;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The Notification.permission property indicates the permission level for the
|
|
||||||
current session and returns one of the following string values:
|
|
||||||
|
|
||||||
'granted': The user has granted permission for notifications.
|
|
||||||
'denied': The user has denied permission for notifications.
|
|
||||||
'default': The user has not made a choice yet.
|
|
||||||
|
|
||||||
Once the user has granted permission, the client application registers a service
|
|
||||||
worker using the `ServiceWorkerRegistration` API.
|
|
||||||
|
|
||||||
The `ServiceWorkerRegistration` API is accessible via the browser's `navigator`
|
|
||||||
object and the `navigator.serviceWorker` child object and ultimately directly
|
|
||||||
accessible via the navigator.serviceWorker.register method which also creates
|
|
||||||
the service worker or the navigator.serviceWorker.getRegistration method.
|
|
||||||
|
|
||||||
Once you have a `ServiceWorkerRegistration` object, that object will provide a
|
|
||||||
child object named `pushManager` through which subscription and management of
|
|
||||||
subscriptions may be done.
|
|
||||||
|
|
||||||
Let's go through the `register` method first:
|
|
||||||
|
|
||||||
```
|
|
||||||
navigator.serviceWorker.register('sw.js', { scope: '/' })
|
|
||||||
.then(function(registration) {
|
|
||||||
console.log('Service worker registered successfully:', registration);
|
|
||||||
})
|
|
||||||
.catch(function(error) {
|
|
||||||
console.log('Service worker registration failed:', error);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
means of communicating between itself and the web page via messages.
|
|
||||||
|
|
||||||
Note that there is a scope can specify what network requests it may
|
|
||||||
intercept.
|
|
||||||
|
|
||||||
The Vue project already has its own service worker but it is possible to
|
|
||||||
create multiple service worker files by registering them on different scopes.
|
|
||||||
|
|
||||||
It is useful architecturally to specify a separate server worker file.
|
|
||||||
|
|
||||||
In the case of web push, the path of the scope only has reference to the domain
|
|
||||||
of the service worker and no relationship to the pathing for the web push
|
|
||||||
server. In order to specify more than one server workers each needs to be on
|
|
||||||
different scope paths!
|
|
||||||
|
|
||||||
Here's a version which can be used for testing locally. Note there can be
|
|
||||||
caching issues in your browser! Incognito is highly recommended.
|
|
||||||
|
|
||||||
sw-dev.ts
|
|
||||||
```
|
|
||||||
self.addEventListener('push', function(event: PushEvent) {
|
|
||||||
console.log('Received a push message', event);
|
|
||||||
|
|
||||||
const title = 'Push message';
|
|
||||||
const body = 'The message body';
|
|
||||||
const icon = '/images/icon-192x192.png';
|
|
||||||
const tag = 'simple-push-demo-notification-tag';
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
self.registration.showNotification(title, {
|
|
||||||
body: body,
|
|
||||||
icon: icon,
|
|
||||||
tag: tag
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
vue.config.js
|
|
||||||
```
|
|
||||||
module.exports = {
|
|
||||||
pwa: {
|
|
||||||
workboxOptions: {
|
|
||||||
importScripts: ['sw-dev.ts']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Once we have the service worker registered and the ServiceWorkerRegistration is
|
|
||||||
returned, we then have access to a `pushManager` property object. This property
|
|
||||||
allows us to continue with the web push work flow.
|
|
||||||
|
|
||||||
In the next step, BROWSER requests a data structure from SERVICE called a VAPID
|
|
||||||
(Voluntary Application Server Identification) which is the public key from a
|
|
||||||
key-pair.
|
|
||||||
|
|
||||||
The VAPID is a specification used to identify the application server (i.e. the
|
|
||||||
SERVICE server) that is sending push messages through a push PROVIDER. It's an
|
|
||||||
authentication mechanism that allows the server to demonstrate its identity to
|
|
||||||
the push PROVIDER, by use of a public and private key pair. These keys are used
|
|
||||||
by the SERVICE in encrypting messages being sent to the BROWSER, as well as
|
|
||||||
being used by the BROWSER in decrypting the messages coming from the SERVICE.
|
|
||||||
|
|
||||||
The VAPID (Voluntary Application Server Identification) key provides more
|
|
||||||
security and authenticity for web push notifications in the following ways:
|
|
||||||
|
|
||||||
Identifying the Application Server:
|
|
||||||
|
|
||||||
The VAPID key is used to identify the application server that is sending
|
|
||||||
the push notifications. This ensures that the push notifications are
|
|
||||||
authentic and not sent by a malicious third party.
|
|
||||||
|
|
||||||
Encrypting the Messages:
|
|
||||||
|
|
||||||
The VAPID key is used to sign the push notifications sent by the
|
|
||||||
application server, ensuring that they are not tampered with during
|
|
||||||
transmission. This provides an additional layer of security and
|
|
||||||
authenticity for the push notifications.
|
|
||||||
|
|
||||||
Adding Contact Information:
|
|
||||||
|
|
||||||
The VAPID key allows a web application to add contact information to
|
|
||||||
the push messages sent to the browser push service. This enables the
|
|
||||||
push service to contact the application server in case of need or
|
|
||||||
provide additional debug information about the push messages.
|
|
||||||
|
|
||||||
Improving Delivery Rates:
|
|
||||||
|
|
||||||
Using the VAPID key can help improve the overall performance of web push
|
|
||||||
notifications, specifically improving delivery rates. By streamlining the
|
|
||||||
delivery process, the chance of delivery errors along the way is lessened.
|
|
||||||
|
|
||||||
If the BROWSER accepts and grants permission to subscribe to receiving from the
|
|
||||||
SERVICE Web Push messages, then the BROWSER makes a subscription request to
|
|
||||||
PROVIDER which creates and stores a special URL for that BROWSER.
|
|
||||||
|
|
||||||
Here's a bit of code describing the above process:
|
|
||||||
|
|
||||||
```
|
|
||||||
// b64 is the VAPID
|
|
||||||
b64 = 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U';
|
|
||||||
const applicationServerKey = urlBase64ToUint8Array(b64);
|
|
||||||
const options: PushSubscriptionOptions = {
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: applicationServerKey
|
|
||||||
};
|
|
||||||
|
|
||||||
registration.pushManager.subscribe(options)
|
|
||||||
.then(function(subscription) {
|
|
||||||
console.log('Push subscription successful:', subscription);
|
|
||||||
})
|
|
||||||
.catch(function(error) {
|
|
||||||
console.error('Push subscription failed:', error);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, the `applicationServerKey` variable contains the VAPID public
|
|
||||||
key, which is converted to a `Uint8Array` using a function such as this:
|
|
||||||
|
|
||||||
```
|
|
||||||
export function toUint8Array(base64String: string, atobFn: typeof atob): Uint8Array {
|
|
||||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
||||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
|
||||||
|
|
||||||
const rawData = atobFn(base64);
|
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return outputArray;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The options object is of type `PushSubscriptionOptions`, which includes the
|
|
||||||
`userVisibleOnly` and `applicationServerKey` (ie VAPID public key) properties.
|
|
||||||
|
|
||||||
options: An object that contains the options used for creating the
|
|
||||||
subscription. This object itself has the following sub-properties:
|
|
||||||
|
|
||||||
applicationServerKey: A public key your push service uses for application
|
|
||||||
server identification. This is normally a Uint8Array.
|
|
||||||
|
|
||||||
userVisibleOnly: A boolean value indicating that the push messages that
|
|
||||||
are sent should be made visible to the user through a notification.
|
|
||||||
This is often set to true.
|
|
||||||
|
|
||||||
The subscribe() method returns a `Promise` that resolves to a `PushSubscription`
|
|
||||||
object containing details of the subscription, such as the endpoint URL and the
|
|
||||||
public key. The returned data would have a form like this:
|
|
||||||
|
|
||||||
{
|
|
||||||
"endpoint": "https://some.pushservice.com/some/unique/identifier",
|
|
||||||
"expirationTime": null,
|
|
||||||
"keys": {
|
|
||||||
"p256dh": "some_base64_encoded_string",
|
|
||||||
"auth": "some_other_base64_encoded_string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint: A string representing the endpoint URL for the push service. This
|
|
||||||
URL is essentially the push service address to which the push message would
|
|
||||||
be sent for this particular subscription.
|
|
||||||
|
|
||||||
expirationTime: A DOMHighResTimeStamp (which is basically a number or null)
|
|
||||||
representing the subscription's expiration time in milliseconds since
|
|
||||||
01 January, 1970 UTC. This can be null if the subscription never expires.
|
|
||||||
|
|
||||||
The BROWSER will, internally, then use that URL to check for incoming messages
|
|
||||||
by way of the service worker we described earlier. The BROWSER also sends this
|
|
||||||
URL back to SERVICE which will use that URL to send messages to the BROWSER via
|
|
||||||
the PROVIDER.
|
|
||||||
|
|
||||||
Ultimately, the actual internal process of receiving messages varies from BROWSER
|
|
||||||
to BROWSER. Approaches vary from long-polling HTTP connections to WebSockets. A
|
|
||||||
lot of handwaving and voodoo magic. The bottom line is that the BROWSER itself
|
|
||||||
manages the connection to the PROVIDER whilst the SERVICE must send messages
|
|
||||||
via the PROVIDER so that they reach the BROWSER service worker.
|
|
||||||
|
|
||||||
Just to remind us that in our service worker our code for receiving messages
|
|
||||||
will look something like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
self.addEventListener('push', function(event: PushEvent) {
|
|
||||||
console.log('Received a push message', event);
|
|
||||||
|
|
||||||
const title = 'Push message';
|
|
||||||
const body = 'The message body';
|
|
||||||
const icon = '/images/icon-192x192.png';
|
|
||||||
const tag = 'simple-push-demo-notification-tag';
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
self.registration.showNotification(title, {
|
|
||||||
body: body,
|
|
||||||
icon: icon,
|
|
||||||
tag: tag
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Now to address the issue of receiving notification messages on mobile devices.
|
|
||||||
It should be noted that Web Push messages are only received when BROWSER is
|
|
||||||
open, except in the cases of Chrome and Firefox mobile BROWSERS. In iOS, the
|
|
||||||
mobile application (in our case a PWA) must be added to the Home Screen and
|
|
||||||
permissions must be explicitly granted that allow the application to receive
|
|
||||||
push notifications. Further, with an iOS device the user must enable wake on
|
|
||||||
notification to have their device light-up when it receives a notification
|
|
||||||
(https://support.apple.com/enus/HT208081).
|
|
||||||
|
|
||||||
So what about #4? - The INTERMEDIARY. Well, It is possible under very special
|
|
||||||
circumstances to create your own Web Push PROVIDER. The only case I've found so
|
|
||||||
far relates to making an Android Custom ROM. (An Android Custom ROM is a
|
|
||||||
customized version of the Android Operating System.) There are open source
|
|
||||||
IMTERMEDIARY products such as UnifiedPush (https://unifiedpush.org/) which can
|
|
||||||
fulfill this role. If you are using iOS you are not permitted to make or use
|
|
||||||
your own custom Web Push PROVIDER. Apple will never allow anyone to do that.
|
|
||||||
Apple has none of its own.
|
|
||||||
|
|
||||||
It is, however, possible to have a sort of proxy working between your SERVICE
|
|
||||||
and FCM (or iOS). Services that mash up various Push notification services (like
|
|
||||||
OneSignal) can perform in the role of such proxies.
|
|
||||||
|
|
||||||
#4 -The INTERMEDIARY- doesn't appear to be anything we should be spending our
|
|
||||||
time on.
|
|
||||||
Reference in New Issue
Block a user