Compare commits

...

25 Commits

Author SHA1 Message Date
Matthew Raymer
e25e5d5ff6 WIPgit add . Updates to package.json ... much breakage 2023-08-30 19:43:26 +08:00
Matthew Raymer
8452af7abc Removing old alerts 2023-08-30 19:17:29 +08:00
43f942c905 Merge pull request 'move ID switcher to advanced section' (#62) from move-id-switch into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#62
2023-08-30 02:43:57 -04:00
8d15b7bfb8 Merge branch 'master' into move-id-switch 2023-08-28 08:20:33 -06:00
5c57ee3e72 move ID switcher to advanced section 2023-08-28 08:18:02 -06:00
Matthew Raymer
3f7bcbfd76 Added unsubscribe and mute 2023-08-28 19:57:47 +08:00
ef0988c9ec Merge pull request 'add link to map on projects which have a location' (#61) from project-map-link into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#61
2023-08-28 01:30:25 -04:00
22de6113e9 Merge branch 'master' into project-map-link 2023-08-28 01:27:30 -04:00
87139f203c Merge pull request 'fix some formatting of web-push doc' (#60) from plan-loc into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#60
2023-08-28 01:26:45 -04:00
c8de13d376 add link to map on projects which have a location 2023-08-27 20:20:16 -06:00
2ccfb283b4 Merge branch 'master' into plan-loc 2023-08-27 13:12:57 -06:00
552fce3281 update the test URL (to remove the port) 2023-08-27 13:12:12 -06:00
12de3dec4f take a stab at the meaning of the "options" in the web-push doc 2023-08-27 11:58:02 -06:00
b171e1ae13 Merge branch 'master' into plan-loc 2023-08-27 11:51:31 -06:00
dc54006fca fix some formatting of web-push doc 2023-08-27 11:47:43 -06:00
9b4db018f5 Merge pull request 'add a location marker to a new plan' (#59) from plan-loc into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#59
2023-08-27 01:07:57 -04:00
519f320a2e add a location marker to a new plan 2023-08-26 21:12:46 -06:00
Matthew Aaron Raymer
f1b3094026 Added a bit more of the workflow on the client side 2023-08-26 16:27:43 +08:00
Matthew Raymer
e5ad87f4d5 Updates to web push guide 2023-08-25 21:45:55 +08:00
Matthew Raymer
7de6171911 Notes on service worker for development of web push 2023-08-24 21:21:15 +08:00
Matthew Raymer
bb6bacac97 Some notes on web-push nuts and bolts 2023-08-24 21:01:31 +08:00
Matthew Raymer
40fc6a29a4 Small fix 2023-08-24 18:49:06 +08:00
Matthew Raymer
9ec19fa4ee Added a quick fix to console signing. Need to edit text later 2023-08-24 18:46:33 +08:00
28b20f86ea Add 'web-push.md'
Document Describing Web Push Workflow
2023-08-24 05:14:17 -04:00
502109de4b Merge pull request 'log bug with "Share Your Info" button' (#58) from increment-derived into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#58
2023-08-24 05:10:47 -04:00
21 changed files with 6773 additions and 5723 deletions

5
.gitignore vendored
View File

@@ -1,13 +1,16 @@
.DS_Store
node_modules
/dist
signature.bin
*.pem
verified.txt
*~
# local env files
.env.local
.env.*.local
# Log files
# Log filesopenssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -1,3 +1,7 @@
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:
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:

25
openssl_signing_console.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/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")

11778
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,58 +9,58 @@
},
"dependencies": {
"@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0",
"@veramo/core": "^5.2.0",
"@veramo/credential-w3c": "^5.2.0",
"@veramo/data-store": "^5.2.0",
"@veramo/did-manager": "^5.1.2",
"@veramo/did-provider-ethr": "^5.1.2",
"@veramo/did-resolver": "^5.2.0",
"@veramo/key-manager": "^5.1.2",
"@vueuse/core": "^10.2.1",
"@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.4.1",
"@veramo/did-manager": "^5.4.1",
"@veramo/did-provider-ethr": "^5.4.1",
"@veramo/did-resolver": "^5.4.1",
"@veramo/key-manager": "^5.4.1",
"@vueuse/core": "^10.4.1",
"@zxing/text-encoding": "^0.9.0",
"axios": "^1.4.0",
"axios": "^1.5.0",
"buffer": "^6.0.3",
"class-transformer": "^0.5.1",
"core-js": "^3.31.1",
"core-js": "^3.32.1",
"dexie": "^3.2.4",
"dexie-export-import": "^4.0.7",
"did-jwt": "^7.2.4",
"ethereum-cryptography": "^2.0.0",
"did-jwt": "^7.2.6",
"ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0",
"ethr-did-resolver": "^8.1.2",
"jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9",
"localstorage-slim": "^2.4.0",
"luxon": "^3.3.0",
"luxon": "^3.4.2",
"merkletreejs": "^0.3.10",
"moment": "^2.29.4",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0",
"pinia-plugin-persistedstate": "^3.2.0",
"qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.0",
"readable-stream": "^4.4.2",
"reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2",
"three": "^0.154.0",
"three": "^0.155.0",
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^2.1.20",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.2.3",
"vue-facing-decorator": "^3.0.2",
"vue-router": "^4.2.4",
"web-did-resolver": "^2.0.27"
},
"devDependencies": {
"@types/ramda": "^0.29.3",
"@types/three": "^0.152.1",
"@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.61.0",
"@types/three": "^0.155.1",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8",
@@ -69,14 +69,15 @@
"@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^5.0.0-alpha.1",
"eslint-plugin-vue": "^9.15.1",
"postcss": "^8.4.24",
"prettier": "^3.0.0",
"tailwindcss": "^3.3.2",
"typescript": "~5.1.6"
"autoprefixer": "^10.4.15",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4",
"postcss": "^8.4.29",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.3",
"typescript": "~5.2.2"
}
}

View File

@@ -2,24 +2,20 @@
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
- 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 } }
- 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- 01 add a location for a project via map pin
- 04 search by a bounding box for local projects (see API by clicking on "Nearby")
- 01 add my bounding box(es) of interest for searches on Nearby part of Discovery page
- .5 search by a bounding box(s) of interest for local projects (see API by clicking on "Nearby")
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
- 02 Fix images on projectview - allow choice of image from a pallete of images or a url image (discovery page display also)
- SEE: https://github.com/dmester/jdenticon assignee:jose
- 08 Scan QR code to import into contacts assignee:matthew
- 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
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew 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
- 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
- SEE: https://github.com/emmanuelsw/notiwind assignee:jose assignee-group:ui
- Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery")
@@ -33,10 +29,9 @@ tasks:
- .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 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 static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose
- .2 on ProjectViewView, show different messages for "to" and "from" sections if none exist assignee-group:ui
- .2 fix static icon to the right on project page (Matthew - I've made "Rotary" into issuer?) assignee:jose assignee-group:ui
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
- .2 move 'switch identity' to the advanced section
- .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"
@@ -45,10 +40,12 @@ tasks:
- 04 allow user to download claims, mine + ones I can see about me from others
- .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)
- .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
- .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 customize favicon assignee-group:ui
- .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?)
- .2 Show a warning if both giver and recipient are the same (but still allow?) 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 assignee-group:ui
- contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings).
@@ -66,7 +63,6 @@ tasks:
- 08 thorough testing for errors & edge cases
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
- Rename DB to TimeSafari.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.

View File

@@ -5,7 +5,7 @@ export enum AppString {
APP_NAME = "Kick-Start with Time",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,

View File

@@ -35,6 +35,7 @@ import {
faGift,
faHand,
faHouseChimney,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
@@ -80,6 +81,7 @@ library.add(
faGift,
faHand,
faHouseChimney,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,

View File

@@ -117,12 +117,6 @@
>
Edit Identity
</router-link>
<router-link
:to="{ name: 'identity-switcher' }"
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Switch Identity / No Identity
</router-link>
<button
@click="
this.$notify(
@@ -238,6 +232,15 @@
</div>
</div>
<div class="flex py-2">
<router-link
:to="{ name: 'identity-switcher' }"
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Switch Identity / No Identity
</router-link>
</div>
<div class="flex py-2">
Claim Server
<input
@@ -283,10 +286,6 @@
</button>
</div>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -300,14 +299,13 @@ import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { AxiosError } from "axios/index";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import { IIdentifier } from "@veramo/core";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
@Component({ components: { AlertMessage, QuickNav } })
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {
Constants = AppString;
@@ -331,8 +329,6 @@ export default class AccountViewView extends Vue {
showPubCopy = false;
showAdvanced = false;
alertMessage = "";
alertTitle = "";
public async getIdentity(activeDid) {
await accountsDB.open();

View File

@@ -90,10 +90,6 @@
</tr>
</tbody>
</table>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -113,17 +109,14 @@ import {
} from "@/libs/endorserServer";
import * as didJwt from "did-jwt";
import { AxiosError } from "axios";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, QuickNav } })
@Component({ components: { QuickNav } })
export default class ContactsView extends Vue {
activeDid = "";
apiServer = "";
contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = [];
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {

View File

@@ -76,10 +76,6 @@
message="Received from"
>
</GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -93,12 +89,11 @@ import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
@Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
components: { GiftedDialog, QuickNav, EntityIcon },
})
export default class HomeView extends Vue {
activeDid = "";
@@ -106,8 +101,6 @@ export default class HomeView extends Vue {
allContacts: Array<Contact> = [];
apiServer = "";
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
accounts: AccountsSchema;
numAccounts = 0;
@@ -289,11 +282,6 @@ export default class HomeView extends Vue {
}
}
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability
isGiveCreationError(result) {

View File

@@ -202,10 +202,6 @@
</li>
</ul>
<p v-else>This identity has no contacts.</p>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -224,7 +220,6 @@ import {
SERVICE_ID,
} from "@/libs/endorserServer";
import { Component, Vue } from "vue-facing-decorator";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
@@ -232,7 +227,7 @@ import EntityIcon from "@/components/EntityIcon";
const Buffer = require("buffer/").Buffer;
@Component({
components: { AlertMessage, QuickNav, EntityIcon },
components: { QuickNav, EntityIcon },
})
export default class ContactsView extends Vue {
activeDid = "";
@@ -256,8 +251,6 @@ export default class ContactsView extends Vue {
showGiveNumbers = false;
showGiveTotals = true;
showGiveConfirmed = true;
alertTitle = "";
alertMessage = "";
async created() {
await db.open();

View File

@@ -103,10 +103,6 @@
</li>
</ul>
</InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -118,13 +114,12 @@ import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { didInfo } from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import InfiniteScroll from "@/components/InfiniteScroll";
import EntityIcon from "@/components/EntityIcon";
@Component({
components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon },
components: { QuickNav, InfiniteScroll, EntityIcon },
})
export default class DiscoverView extends Vue {
activeDid = "";
@@ -132,8 +127,6 @@ export default class DiscoverView extends Vue {
allMyDids: Array<string> = [];
apiServer = "";
searchTerms = "";
alertMessage = "";
alertTitle = "";
projects: ProjectData[] = [];
isLocalActive = true;
isRemoteActive = false;

View File

@@ -111,7 +111,7 @@
<div class="mb-8">
<h2 class="text-xl font-bold">Quick Action</h2>
<p class="mb-4">Show appreciation to a contact:</p>
<p class="mb-4">Record a gift from a contact:</p>
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<li @click="openDialog()">
@@ -196,10 +196,6 @@
</li>
</ul>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -211,12 +207,11 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
@Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
components: { GiftedDialog, QuickNav, EntityIcon },
})
export default class HomeView extends Vue {
activeDid = "";
@@ -228,8 +223,6 @@ export default class HomeView extends Vue {
feedPreviousOldestId = null;
feedLastViewedId = null;
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {
@@ -509,11 +502,6 @@ export default class HomeView extends Vue {
}
}
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability
isGiveCreationError(result) {

View File

@@ -62,11 +62,6 @@
>
No Identity
</a>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
@@ -75,18 +70,15 @@ import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, QuickNav } })
@Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue {
Constants = AppString;
public accounts: AccountsSchema;
public activeDid;
public firstName;
public lastName;
public alertTitle;
public alertMessage;
public otherIdentities = [];
public async getIdentity(activeDid) {

View File

@@ -39,6 +39,40 @@
{{ description.length }}/500 max. characters
</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">
<button
:disabled="isHiddenSave"
@@ -63,37 +97,36 @@
Cancel
</button>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core";
import AlertMessage from "@/components/AlertMessage";
@Component({
components: { AlertMessage },
components: { LMap, LMarker, LTileLayer },
})
export default class NewEditProjectView extends Vue {
activeDid = "";
apiServer = "";
projectName = "";
description = "";
errorMessage = "";
includeLocation = false;
latitude = 0;
longitude = 0;
numAccounts = 0;
alertTitle = "";
alertMessage = "";
projectName = "";
zoom = 2;
async beforeCreate() {
await accountsDB.open();
@@ -185,6 +218,15 @@ export default class NewEditProjectView extends Vue {
if (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
const vcPayload = {
vc: {
@@ -221,8 +263,6 @@ export default class NewEditProjectView extends Vue {
// version shows up here: https://api.endorser.ch/api-docs/
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
this.errorMessage = "";
this.alertTitle = "";
this.alertMessage = "";
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://api.endorser.ch/api-docs/
useAppStore().setProjectId(
@@ -299,6 +339,14 @@ 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() {
this.$router.back();
}

View File

@@ -39,6 +39,16 @@
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
{{ timeSince }}
</div>
<div v-if="latitude || longitude">
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
<a
:href="getOpenStreetMapUrl()"
target="_blank"
class="underline"
>
Map View
</a>
</div>
</div>
</div>
</div>
@@ -190,10 +200,6 @@
message="Received from"
>
</GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -213,17 +219,14 @@ import {
didInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
@Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon },
components: { GiftedDialog, QuickNav, EntityIcon },
})
export default class ProjectViewView extends Vue {
activeDid = "";
alertMessage = "";
alertTitle = "";
allMyDids: Array<string> = [];
allContacts: Array<Contact> = [];
apiServer = "";
@@ -231,6 +234,8 @@ export default class ProjectViewView extends Vue {
expanded = false;
givesToThis: Array<GiveServerRecord> = [];
givesByThis: Array<GiveServerRecord> = [];
latitude = 0;
longitude = 0;
name = "";
issuer = "";
projectId = localStorage.getItem("projectId") || ""; // handle ID
@@ -326,6 +331,8 @@ export default class ProjectViewView extends Vue {
this.name = resp.data.claim?.name || "(no name)";
this.description = resp.data.claim?.description || "(no description)";
this.truncatedDesc = this.description.slice(0, this.truncateLength);
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
} else if (resp.status === 404) {
// actually, axios throws an error so we never get here
this.$notify(
@@ -441,6 +448,20 @@ export default class ProjectViewView extends Vue {
this.$refs.customDialog.open(contact);
}
getOpenStreetMapUrl() {
// Google URL is https://maps.google.com/?q=LAT,LONG
return (
"https://www.openstreetmap.org/?mlat=" +
this.latitude +
"&mlon=" +
this.longitude +
"#map=15/" +
this.latitude +
"/" +
this.longitude
);
}
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {

View File

@@ -67,10 +67,6 @@
</li>
</ul>
</InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -81,20 +77,17 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon";
@Component({
components: { InfiniteScroll, AlertMessage, QuickNav, EntityIcon },
components: { InfiniteScroll, QuickNav, EntityIcon },
})
export default class ProjectsView extends Vue {
apiServer = "";
projects: ProjectData[] = [];
current: IIdentifier;
isLoading = false;
alertTitle = "";
alertMessage = "";
numAccounts = 0;
async beforeCreate() {

View File

@@ -50,10 +50,6 @@
</div>
</div>
<div v-else>You do not have an active identity.</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
@@ -62,16 +58,13 @@ import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import * as R from "ramda";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, QuickNav } })
@Component({ components: { QuickNav } })
export default class SeedBackupView extends Vue {
activeAccount = null;
numAccounts = 0;
showSeed = false;
alertMessage = "";
alertTitle = "";
// 'created' hook runs when the Vue instance is first created
async created() {

View File

@@ -34,25 +34,18 @@
</div>
<button class="float-right" @click="captureGraphics()">Screenshot</button>
<div id="scene-container" class="h-screen"></div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator";
import { World } from "@/components/World/World.js";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, World, QuickNav } })
@Component({ components: { World, QuickNav } })
export default class StatisticsView extends Vue {
world: World;
worldProperties: WorldProperties = {};
alertTitle = "";
alertMessage = "";
mounted() {
try {

360
web-push.md Normal file
View File

@@ -0,0 +1,360 @@
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.
A BROWSER may also remove a subscription. In order to remove a subscription,
the registration record must be retrieved from the serviceWorker using
`navigator.serviceWorker.ready`. Within the `ready` property is the
`pushManager` which has a `getSubscription` method. Once you have the
subscription object, you may call the `unsubscribe` method. `unsubscribe` is
asynchronnous and returns a boolean true if it is successful in removing the
subscription and false if not.
```
async function unsubscribeFromPush() {
// Check if the browser supports service workers
if ("serviceWorker" in navigator) {
// Get the registration object for the service worker
const registration = await navigator.serviceWorker.ready;
// Get the existing subscription
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
// Unsubscribe
const successful = await subscription.unsubscribe();
if (successful) {
console.log("Successfully unsubscribed from push notifications.");
// You can also inform your server to remove this subscription
} else {
console.log("Failed to unsubscribe from push notifications.");
}
} else {
console.log("No subscription was found.");
}
} else {
console.log("Service workers are not supported by this browser.");
}
}
// Unsubscribe from push notifications
unsubscribeFromPush().catch((err) => {
console.error("An error occurred while unsubscribing from push notifications", err);
});
```
NOTE: We could offer an option within the app to "mute" these notifications. This wouldn't turn off the notifications at the browser level, but you could make it so that your Service Worker doesn't display them even if it receives them.