Browse Source

Merge pull request 'Daily service-worker logging' (#100) from sw-log into master

Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/100
starred-projects
trentlarson 9 months ago
parent
commit
ddbb700c34
  1. 20
      README.md
  2. 2
      package.json
  3. 39
      project.task.yaml
  4. 28
      src/App.vue
  5. 4
      src/components/GiftedDialog.vue
  6. 4
      src/components/OfferDialog.vue
  7. 2
      src/components/TopMessage.vue
  8. 12
      src/constants/app.ts
  9. 18
      src/db/index.ts
  10. 2
      src/db/tables/contacts.ts
  11. 10
      src/db/tables/logs.ts
  12. 18
      src/views/AccountViewView.vue
  13. 3
      src/views/ContactAmountsView.vue
  14. 3
      src/views/ContactGiftingView.vue
  15. 170
      src/views/HelpNotificationsView.vue
  16. 9
      src/views/HelpView.vue
  17. 3
      src/views/HomeView.vue
  18. 66
      sw_scripts/additional-scripts.js
  19. 174
      sw_scripts/safari-notifications.js

20
README.md

@ -22,25 +22,25 @@ npm run lint
If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",` If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
* `npx prettier --write ./sw_scripts/`
...to make sure the service worker scripts are in proper form
* Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit. * Update the CHANGELOG.md & the version in package.json, run `npm install`, and commit.
* Tag wth the new version: `git tag 0.1.0`. * Tag wth the new version: `git tag 0.1.0`.
* If production, change src/constants/app.ts DEFAULT_*_SERVER to be PROD. * If production, change src/constants/app.ts DEFAULT_*_SERVER to be "PROD" and package.json to remove "_Test".
* `npm run build` * `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/` * `cp sw_scripts/[ns]* dist/`
... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js. ... to copy the contents of the `sw_scripts` folder to the `dist` folder - except additional_scripts.js.
* `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari` * `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntu@endorser.ch:time-safari`
* Revert src/constants/app.ts, increment version, add "-beta", `npm install`, and commit. * Revert src/constants/app.ts and/or package.json, edit package.json to increment version & add "-beta", `npm install`, and commit.
@ -112,10 +112,10 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
### Clear/Reset data & restart ### Clear/Reset 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".) * 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.)
* Unregister service worker (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`). * Clear notification permission. (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications".)
* Clear notification permission (in Chrome, go to `chrome://settings/content/notifications`; in Firefox, go to `about:preferences` and search for "notifications"). * Unregister service worker. (in Chrome, go to `chrome://serviceworker-internals/`; in Firefox, go to `about:serviceworkers`.)
* Clear Cache Storage (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage). * Clear Cache Storage. (in Chrome, in dev tools under Application; in Firefox, in dev tools under Storage.)

2
package.json

@ -1,5 +1,5 @@
{ {
"name": "TimeSafari", "name": "TimeSafari_Test",
"version": "0.1.8-beta", "version": "0.1.8-beta",
"private": true, "private": true,
"scripts": { "scripts": {

39
project.task.yaml

@ -2,29 +2,30 @@
tasks: tasks:
- 08 notifications : - 08 notifications :
- make the app behave correctly when App Notifications are turned off - get the rest of our Android devices to work
- write troubleshooting docs for notifications - insert tooling (exportable logs?) so that we can see problems and troubleshoot as we onboard
- hide the "App Notifications" toggle when they switch notifications - get an error registering notifications on Firefox and subscription info is null
- prompt user to install on their home screen https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps - if navigator.serviceWorker is null, then tell the user to wait
- Android DuckDuckGo asked for my permissions, got error, won't download DB
- Android Chrome won't ask permission, will download log but always empty
- Firefox works
- Local install works after cleared out cache in Chrome
- create troubleshooting notification:
- server gets signal to send a normal notification back immediately
- add windows & mac help at OS & browser level, in HelpNotificationsView.vue (linked from account page)
maybe tell them to pause, after first turn-on and after test
maybe Google Play permissions
- prompt user to install on their home screen https://benborgers.com/posts/pwa-detect-installed
- warn if they're using the web (android only?) - warn if they're using the web (android only?)
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getInstalledRelatedApps
https://web.dev/articles/get-installed-related-apps https://web.dev/articles/get-installed-related-apps
- add windows & mac help at OS & browser level
- 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 - .2 fix the projects on /discover to show the issuer (currently all "Someone Anonymous")
- fix the projects on /discover to show the issuer (currently all "Someone Anonymous")
- .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui - .3 bug - make or edit a project, choose "Include location", and see the map display shows on top of the bottom icons assignee-group:ui
- Got error adding on Firefox user #0 as contact for themselves
- .5 If notifications are not enabled, add message to front page with link/button to enable - .5 If notifications are not enabled, add message to front page with link/button to enable
- 01 server - show all claim details when issued by the issuer
- enhance help page instructions for friends to get notifications, for debugging notifications
- add way to test a push notification
- help instructions for PWA install problems (secret failed, must reinstall)
- look at other examples for better UI friend.tech
- show VC details... somehow: - show VC details... somehow:
- 01 show my VCs - most interesting, or via search - 01 show my VCs - most interesting, or via search
- 01 allow download of each VC (& confirmations, to show that they actually own their data) - 01 allow download of each VC (& confirmations, to show that they actually own their data)
@ -44,9 +45,14 @@ tasks:
- Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts - Other features - donation vs give, show offers, show give & outstanding totals, show network view, restrict registration, connect to contacts
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- 01 send visibility signal as a VC and store it
- remove 'rowid' references (that are sqlite-specific)
- make identicons for contacts into more-memorable faces (and maybe change project identicons, too) - make identicons for contacts into more-memorable faces (and maybe change project identicons, too)
- 04 split out notification logic & tests from web-push-subscription logic & tests
- 01 server - show all claim details when issued by the issuer
- bug - got error adding on Firefox user #0 as contact for themselves
- bug - 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 send visibility signal as a VC and store it
- 04 remove 'rowid' references (that are sqlite-specific); may involve server
- 04 look at other examples for better UI friend.tech
- 01 make the prod build copy the sw_scripts - 01 make the prod build copy the sw_scripts
- .5 Add start date to project - .5 Add start date to project
- .3 check that Android shows "back" buttons on screens without bottom tray - .3 check that Android shows "back" buttons on screens without bottom tray
@ -83,6 +89,7 @@ tasks:
- 24 Move to Vite - 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
- 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

28
src/App.vue

@ -281,7 +281,7 @@ interface VapidResponse {
}; };
} }
import { AppString } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@ -302,7 +302,7 @@ export default class App extends Vue {
try { try {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
let pushUrl: string = AppString.DEFAULT_PUSH_SERVER; let pushUrl = DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) { if (settings?.webPushServer) {
pushUrl = settings.webPushServer; pushUrl = settings.webPushServer;
} }
@ -373,6 +373,7 @@ export default class App extends Vue {
} }
private askPermission(): Promise<NotificationPermission> { private askPermission(): Promise<NotificationPermission> {
console.log("Requesting permission for notifications:", navigator);
if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) { if (!("serviceWorker" in navigator && navigator.serviceWorker.controller)) {
return Promise.reject("Service worker not available."); return Promise.reject("Service worker not available.");
} }
@ -445,7 +446,18 @@ export default class App extends Vue {
} }
}) })
.then(() => { .then(() => {
console.log("Subscription data sent to server."); console.log(
"Subscription data sent to server and all finished successfully.",
);
this.$notify(
{
group: "alert",
type: "success",
title: "Notifications Turned On",
text: "Notifications are on. You should see one on your device; if not, see the 'Troubleshoot' page.",
},
-1,
);
}) })
.catch((error) => { .catch((error) => {
console.error( console.error(
@ -462,9 +474,7 @@ export default class App extends Vue {
"An error occurred setting notification permissions:", "An error occurred setting notification permissions:",
error, error,
); );
alert( alert("Some error occurred setting notification permissions.");
"Some error occurred setting notification permissions. See logs.",
);
}); });
} }
@ -511,11 +521,7 @@ export default class App extends Vue {
resolve(); resolve();
}) })
.catch((error) => { .catch((error) => {
console.error( console.error("Push subscription failed:", error, options);
"Subscription or server communication failed:",
error,
options,
);
// Inform the user about the issue // Inform the user about the issue
alert( alert(

4
src/components/GiftedDialog.vue

@ -123,9 +123,7 @@ export default class GiftedDialog extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text: err.message || "There was an error retrieving your settings.",
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );

4
src/components/OfferDialog.vue

@ -105,9 +105,7 @@ export default class OfferDialog extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text: err.message || "There was an error retrieving your settings.",
err.message ||
"There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
); );

2
src/components/TopMessage.vue

@ -50,7 +50,7 @@ export default class TopMessage extends Vue {
title: "Error Detecting Server", title: "Error Detecting Server",
text: JSON.stringify(err), text: JSON.stringify(err),
}, },
10000, -1,
); );
} }
} }

12
src/constants/app.ts

@ -4,22 +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",
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
PROD_PUSH_SERVER = "https://timesafari.app", PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app", TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com", TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
DEFAULT_PUSH_SERVER = TEST1_PUSH_SERVER,
} }
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. * The possible values for "group" and "type" are in App.vue.
* From the notiwind package * From the notiwind package

18
src/db/index.ts

@ -1,18 +1,20 @@
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, ContactsSchema } from "./tables/contacts"; import { Contact, ContactSchema } from "./tables/contacts";
import { Log, LogSchema } from "@/db/tables/logs";
import { import {
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
Settings, Settings,
SettingsSchema, SettingsSchema,
} from "./tables/settings"; } from "./tables/settings";
import { AppString } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
// Define types for tables that hold sensitive and non-sensitive data // Define types for tables that hold sensitive and non-sensitive data
type SensitiveTables = { accounts: Table<Account> }; type SensitiveTables = { accounts: Table<Account> };
type NonsensitiveTables = { type NonsensitiveTables = {
contacts: Table<Contact>; contacts: Table<Contact>;
logs: Table<Log>;
settings: Table<Settings>; settings: Table<Settings>;
}; };
@ -26,7 +28,11 @@ export const accountsDB = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
const SensitiveSchemas = { ...AccountsSchema }; const SensitiveSchemas = { ...AccountsSchema };
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie; export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
const NonsensitiveSchemas = { ...ContactsSchema, ...SettingsSchema }; const NonsensitiveSchemas = {
...ContactSchema,
...LogSchema,
...SettingsSchema,
};
// Manage the encryption key. If not present in localStorage, create and store it. // Manage the encryption key. If not present in localStorage, create and store it.
const secret = const secret =
@ -38,12 +44,14 @@ encrypted(accountsDB, { secretKey: secret });
// Define the schema for our databases // Define the schema for our databases
accountsDB.version(1).stores(SensitiveSchemas); accountsDB.version(1).stores(SensitiveSchemas);
db.version(1).stores(NonsensitiveSchemas); // v1 was contacts & settings
// v2 added logs
db.version(2).stores(NonsensitiveSchemas);
// Event handler to initialize the non-sensitive database with default settings // Event handler to initialize the non-sensitive database with default settings
db.on("populate", () => { db.on("populate", () => {
db.settings.add({ db.settings.add({
id: MASTER_SETTINGS_KEY, id: MASTER_SETTINGS_KEY,
apiServer: AppString.DEFAULT_ENDORSER_API_SERVER, apiServer: DEFAULT_ENDORSER_API_SERVER,
}); });
}); });

2
src/db/tables/contacts.ts

@ -6,6 +6,6 @@ export interface Contact {
registered?: boolean; registered?: boolean;
} }
export const ContactsSchema = { export const ContactSchema = {
contacts: "&did, name, publicKeyBase64, registered, seesMe", contacts: "&did, name, publicKeyBase64, registered, seesMe",
}; };

10
src/db/tables/logs.ts

@ -0,0 +1,10 @@
export interface Log {
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, message",
};

18
src/views/AccountViewView.vue

@ -133,6 +133,9 @@
Notification status may have changed. Revisit this page to see the Notification status may have changed. Revisit this page to see the
latest setting. latest setting.
</div> </div>
<router-link class="px-4 text-sm text-blue-500" to="/help-notifications">
Troubleshoot notifications here.
</router-link>
</div> </div>
<h3 class="text-sm uppercase font-semibold mb-3">Data</h3> <h3 class="text-sm uppercase font-semibold mb-3">Data</h3>
@ -306,13 +309,6 @@
Switch Identity Switch Identity
</router-link> </router-link>
<button
@click="alertWebPushSubscription()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Show Subscription from Web Push Server
</button>
<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>
<input <input
@ -1048,13 +1044,5 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
} }
alertWebPushSubscription() {
console.log(
"Web push subscription:",
JSON.parse(JSON.stringify(this.subscription)), // gives more info than plain console logging
);
alert(JSON.stringify(this.subscription));
}
} }
</script> </script>

3
src/views/ContactAmountsView.vue

@ -178,6 +178,7 @@ 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",
@ -185,7 +186,7 @@ export default class ContactAmountssView extends Vue {
title: "Error", title: "Error",
text: text:
err.userMessage || err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.", "There was an error retrieving your settings and/or contacts and/or gives.",
}, },
-1, -1,
); );

3
src/views/ContactGiftingView.vue

@ -145,6 +145,7 @@ 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",
@ -152,7 +153,7 @@ export default class ContactGiftingView extends Vue {
title: "Error", title: "Error",
text: text:
err.message || err.message ||
"There was an error retrieving the latest sweet, sweet action.", "There was an error retrieving your settings and/or contacts.",
}, },
-1, -1,
); );

170
src/views/HelpNotificationsView.vue

@ -22,10 +22,16 @@
</div> </div>
<div> <div>
<p>Here are things to try to get notifications working.</p> <p>Here are ways to get notifications working.</p>
<h2 class="text-xl font-semibold">Test</h2> <h2 class="text-xl font-semibold">Test</h2>
<p>Somehow call the service-worker self.showNotification</p> <p>Somehow call the service-worker self.showNotification</p>
<button
@click="showTestNotification()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
See a Test Notification
</button>
<h2 class="text-xl font-semibold">Check OS-level permissions</h2> <h2 class="text-xl font-semibold">Check OS-level permissions</h2>
<div> <div>
@ -54,7 +60,10 @@
<div>Make sure your OS (above) supports it.</div> <div>Make sure your OS (above) supports it.</div>
<h3 class="text-lg font-semibold">Mobile Phone - Android</h3> <h3 class="text-lg font-semibold">Mobile Phone - Android</h3>
<div>Chrome requires version 50.</div> <div>
Chrome requires version 50. Hold icon, select "App info", and enable
notifications.
</div>
</div> </div>
</div> </div>
@ -66,12 +75,34 @@
<h2 class="text-xl font-semibold">Auto-detection</h2> <h2 class="text-xl font-semibold">Auto-detection</h2>
<p>Show results of auto-detection whether they're turned on</p> <p>Show results of auto-detection whether they're turned on</p>
<button
@click="alertWebPushSubscription()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Show Subscription from Web Push Server
</button>
<button
@click="sendTestWebPushMessage()"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
Send Myself a Test Web Push Message
</button>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { DEFAULT_PUSH_SERVER } from "@/constants/app";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
interface Notification { interface Notification {
group: string; group: string;
@ -83,5 +114,140 @@ interface Notification {
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class HelpNotificationsView extends Vue { export default class HelpNotificationsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $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() {
if (!this.subscription) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Not Subscribed",
text: "You must enable notifications before testing the web push.",
},
-1,
);
return;
}
try {
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 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(this.subscription.getKey("auth"));
const authB64 = auth
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const p256dh = Buffer.from(this.subscription.getKey("p256dh"));
const p256dhB64 = p256dh
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const newPayload = {
endpoint: this.subscription.endpoint,
keys: {
auth: authB64,
p256dh: p256dhB64,
},
message: "This is a test message, triggered by you.",
title: DIRECT_PUSH_TITLE,
};
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);
this.$notify(
{
group: "alert",
type: "success",
title: "Test Web Push Sent",
text: "Check your device for the test web push message.",
},
-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() {
navigator.serviceWorker.ready
.then((registration) => {
return registration.showNotification("It Worked", {
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 soon.",
},
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,
);
});
}
} }
</script> </script>

9
src/views/HelpView.vue

@ -182,6 +182,15 @@
different page. different page.
</p> </p>
<h2 class="text-xl font-semibold">
Where do I get help with notifications?
</h2>
<p>
<router-link class="text-blue-500" to="/help-notifications"
>Here.</router-link
>
</p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I access even more functionality? How do I access even more functionality?
</h2> </h2>

3
src/views/HomeView.vue

@ -234,6 +234,7 @@ export default class HomeView 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 and/or feed.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -241,7 +242,7 @@ export default class HomeView extends Vue {
title: "Error", title: "Error",
text: text:
err.userMessage || err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.", "There was an error retrieving your settings and/or the latest activity.",
}, },
-1, -1,
); );

66
sw_scripts/additional-scripts.js

@ -5,67 +5,103 @@ importScripts(
); );
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
console.log(new Date().toISOString(), "Installing service worker:", event); console.log("Service worker got install. Importing scripts...", event);
importScripts( importScripts(
"safari-notifications.js", "safari-notifications.js",
"nacl.js", "nacl.js",
"noble-curves.js", "noble-curves.js",
"noble-hashes.js", "noble-hashes.js",
); );
console.log("Service worker imported scripts.");
}); });
function logConsoleAndDb(message, arg1, arg2) {
console.log(`${new Date().toISOString()} ${message}`, arg1, arg2);
let fullMessage = `${new Date().toISOString()} ${message}`;
if (arg1) {
fullMessage += `\n${JSON.stringify(arg1)}`;
}
if (arg2) {
fullMessage += `\n${JSON.stringify(arg2)}`;
}
self.appendDailyLog(fullMessage);
}
self.addEventListener("push", function (event) { self.addEventListener("push", function (event) {
console.log(new Date().toISOString(), "Received push event:", event); let text = null;
if (event.data) {
text = event.data.text();
}
logConsoleAndDb("Service worker received a push event.", text, event);
event.waitUntil( event.waitUntil(
(async () => { (async () => {
try { try {
let payload; let payload;
if (event.data) { if (text) {
payload = JSON.parse(event.data.text()); try {
payload = JSON.parse(text);
} catch (e) {
// don't use payload since it is not JSON
}
}
// This is shared with the notification-test code and should be a constant. Look for the same name in HelpNotificationsView.vue
// 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";
let title = "Generic Notification";
let message = "Got some empty message.";
if (payload && payload.title == DIRECT_PUSH_TITLE) {
// skip any search logic and show the message directly
title = "Direct Message";
message = payload.message || "No details were provided.";
} else {
title =
payload && payload.title ? payload.title : "Unknown Notification";
message = await self.getNotificationCount();
} }
const message = await self.getNotificationCount();
if (message) { if (message) {
const title = payload && payload.title ? payload.title : "Message";
const options = { const options = {
body: message, body: message,
icon: payload ? payload.icon : "icon.png", icon: payload ? payload.icon : "icon.png",
badge: payload ? payload.badge : "badge.png", badge: payload ? payload.badge : "badge.png",
}; };
await self.registration.showNotification(title, options); await self.registration.showNotification(title, options);
console.log(new Date().toISOString(), "Notified user:", options); logConsoleAndDb("Notified user:", options);
} else { } else {
console.log(new Date().toISOString(), "No notification message."); logConsoleAndDb("No notification message.");
} }
} catch (error) { } catch (error) {
console.error(new Date().toISOString(), "Error with push event", event, " - ", error); logConsoleAndDb("Error with push event", event, error);
} }
})(), })(),
); );
}); });
self.addEventListener("message", (event) => { self.addEventListener("message", (event) => {
console.log(new Date().toISOString(), "Service worker message:", event); logConsoleAndDb("Service worker got a message...", event);
if (event.data && event.data.type === "SEND_LOCAL_DATA") { if (event.data && event.data.type === "SEND_LOCAL_DATA") {
self.secret = event.data.data; self.secret = event.data.data;
event.ports[0].postMessage({ success: true }); event.ports[0].postMessage({ success: true });
} }
console.log(new Date().toISOString(), "Service worker posted message."); logConsoleAndDb("Service worker posted a message.");
}); });
self.addEventListener("activate", (event) => { self.addEventListener("activate", (event) => {
console.log(new Date().toISOString(), "Service worker activating...", event); logConsoleAndDb("Service worker activating...", event);
// see https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim // see https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
// and https://web.dev/articles/service-worker-lifecycle#clientsclaim // and https://web.dev/articles/service-worker-lifecycle#clientsclaim
event.waitUntil(clients.claim()); event.waitUntil(clients.claim());
console.log(new Date().toISOString(), "Service worker activated."); logConsoleAndDb("Service worker is activated.");
}); });
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
console.log(new Date().toISOString(), "Got fetch event:", event); logConsoleAndDb("Service worker got fetch event.", event);
}); });
self.addEventListener("error", (event) => { self.addEventListener("error", (event) => {
console.error(new Date().toISOString(), "Error in Service Worker:", event); logConsoleAndDb("Service worker error", event);
console.error("Full Error:", event);
console.error("Message:", event.message); console.error("Message:", event.message);
console.error("File:", event.filename); console.error("File:", event.filename);
console.error("Line:", event.lineno); console.error("Line:", event.lineno);

174
sw_scripts/safari-notifications.js

@ -395,12 +395,42 @@ async function setMostRecentNotified(id) {
data["lastNotifiedClaimId"] = id; data["lastNotifiedClaimId"] = id;
await updateRecord(store, data); await updateRecord(store, data);
} else { } else {
console.error("IndexedDB settings record not found."); console.error(
"safari-notifications setMostRecentNotified IndexedDB settings record not found",
);
} }
transaction.oncomplete = () => db.close(); transaction.oncomplete = () => db.close();
} catch (error) { } catch (error) {
console.error("IndexedDB error:", 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");
// will 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 anything older than today
}
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;
} }
} }
@ -420,6 +450,7 @@ function getRecord(store, key) {
}); });
} }
// Note that this assumes there is only one record in the store.
function updateRecord(store, data) { function updateRecord(store, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = store.put(data); const request = store.put(data);
@ -430,20 +461,23 @@ function updateRecord(store, data) {
async function fetchAllAccounts() { async function fetchAllAccounts() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let openRequest = indexedDB.open("TimeSafariAccounts"); const openRequest = indexedDB.open("TimeSafariAccounts");
openRequest.onupgradeneeded = function (event) { openRequest.onupgradeneeded = function (event) {
let db = event.target.result; const db = event.target.result;
if (!db.objectStoreNames.contains("accounts")) { if (!db.objectStoreNames.contains("accounts")) {
db.createObjectStore("accounts", { keyPath: "id" }); db.createObjectStore("accounts", { keyPath: "id" });
} }
if (!db.objectStoreNames.contains("worker_log")) {
db.createObjectStore("worker_log");
}
}; };
openRequest.onsuccess = function (event) { openRequest.onsuccess = function (event) {
let db = event.target.result; const db = event.target.result;
let transaction = db.transaction("accounts", "readonly"); const transaction = db.transaction("accounts", "readonly");
let objectStore = transaction.objectStore("accounts"); const objectStore = transaction.objectStore("accounts");
let getAllRequest = objectStore.getAll(); const getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = function () { getAllRequest.onsuccess = function () {
resolve(getAllRequest.result); resolve(getAllRequest.result);
@ -460,76 +494,74 @@ async function fetchAllAccounts() {
} }
async function getNotificationCount() { async function getNotificationCount() {
let secret = null;
let accounts = []; let accounts = [];
let result = null; let result = null;
if ("secret" in self) { // 1 is our master settings ID; see MASTER_SETTINGS_KEY
secret = self.secret; const settings = await getSettingById(1);
const secretUint8Array = self.decodeBase64(secret); let lastNotifiedClaimId = null;
// 1 is our master settings ID; see MASTER_SETTINGS_KEY if ("lastNotifiedClaimId" in settings) {
const settings = await getSettingById(1); lastNotifiedClaimId = settings["lastNotifiedClaimId"];
let lastNotifiedClaimId = null; }
if ("lastNotifiedClaimId" in settings) { const activeDid = settings["activeDid"];
lastNotifiedClaimId = settings["lastNotifiedClaimId"]; accounts = await fetchAllAccounts();
let activeAccount = null;
for (let i = 0; i < accounts.length; i++) {
if (accounts[i]["did"] == activeDid) {
activeAccount = accounts[i];
break;
} }
const activeDid = settings["activeDid"]; }
accounts = await fetchAllAccounts();
let did = null; const headers = {
for (var i = 0; i < accounts.length; i++) { "Content-Type": "application/json",
let account = accounts[i]; };
let did = account["did"];
if (did == activeDid) { const identity = activeAccount && activeAccount["identity"];
let publicKeyHex = account["publicKeyHex"]; if (identity && "secret" in self) {
let identity = account["identity"]; const secret = self.secret;
const messageWithNonceAsUint8Array = self.decodeBase64(identity); const secretUint8Array = self.decodeBase64(secret);
const nonce = messageWithNonceAsUint8Array.slice(0, 24); const messageWithNonceAsUint8Array = self.decodeBase64(identity);
const message = messageWithNonceAsUint8Array.slice(24, identity.length); const nonce = messageWithNonceAsUint8Array.slice(0, 24);
const decoder = new TextDecoder("utf-8"); const message = messageWithNonceAsUint8Array.slice(24, identity.length);
const decrypted = self.secretbox.open(message, nonce, secretUint8Array); const decoder = new TextDecoder("utf-8");
const decrypted = self.secretbox.open(message, nonce, secretUint8Array);
const msg = decoder.decode(decrypted); const msg = decoder.decode(decrypted);
const identifier = JSON.parse(JSON.parse(msg)); const identifier = JSON.parse(JSON.parse(msg));
const headers = { headers["Authorization"] = "Bearer " + (await accessToken(identifier));
"Content-Type": "application/json", }
};
const response = await fetch(
headers["Authorization"] = "Bearer " + (await accessToken(identifier)); settings["apiServer"] + "/api/v2/report/claims",
{
let response = await fetch( method: "GET",
settings["apiServer"] + "/api/v2/report/claims", headers: headers,
{ },
method: "GET", );
headers: headers, if (response.status == 200) {
}, const json = await response.json();
); const claims = json["data"];
if (response.status == 200) { let newClaims = 0;
let json = await response.json(); for (let i = 0; i < claims.length; i++) {
let claims = json["data"]; const claim = claims[i];
let newClaims = 0; if (claim["id"] === lastNotifiedClaimId) {
for (var i = 0; i < claims.length; i++) {
let claim = claims[i];
if (claim["id"] === lastNotifiedClaimId) {
break;
}
newClaims++;
}
if (newClaims > 0) {
result = `There are ${newClaims} new activities on TimeSafari`;
}
const most_recent_notified = claims[0]["id"];
await setMostRecentNotified(most_recent_notified);
} else {
console.error(
"The service worker got a bad response status when fetching claims:",
response.status,
response,
);
}
break; 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; return result;
} }

Loading…
Cancel
Save