Browse Source

refactor: reorganize deep linking types and interfaces

Changes:
- Move deep link types from types/ to interfaces/
- Export baseUrlSchema for external use
- Add trailing commas for better git diffs
- Fix type inference for deepLinkSchemas
- Add deepLinks export to interfaces/index.ts
- Remove duplicate SuccessResult interface
- Update import paths in services/deepLinks.ts

This improves code organization by centralizing interface definitions
and fixing type inference issues.
side_step
Matthew Raymer 2 weeks ago
parent
commit
20620c3aae
  1. 33
      README.md
  2. 915
      package-lock.json
  3. 2
      package.json
  4. 5
      src/interfaces/common.ts
  5. 13
      src/interfaces/deepLinks.ts
  6. 1
      src/interfaces/index.ts
  7. 9
      src/main.capacitor.ts
  8. 83
      src/services/deepLinks.ts
  9. 20
      src/types/deepLinks.ts
  10. 93
      web-push.md

33
README.md

@ -8,8 +8,6 @@ and expand to crowd-fund with time & money, then record and see the impact of co
See [project.task.yaml](project.task.yaml) for current priorities. See [project.task.yaml](project.task.yaml) for current priorities.
(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.) (Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
## Setup & Building ## Setup & Building
Quick start: Quick start:
@ -19,15 +17,17 @@ npm install
npm run dev npm run dev
``` ```
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use <http://localhost:3000> for local endorser.ch
### Build the test & production app ### Build the test & production app
```
```bash
npm run serve npm run serve
``` ```
### Lint and fix files ### Lint and fix files
```
```bash
npm run lint npm run lint
``` ```
@ -35,7 +35,6 @@ npm run lint
Look below for the "test-all" instructions. Look below for the "test-all" instructions.
### Compile and minify for test & production ### Compile and minify for test & production
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations. * If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
@ -52,15 +51,19 @@ Look below for the "test-all" instructions.
* For test, build the app (because test server is not yet set up to build): * For test, build the app (because test server is not yet set up to build):
``` ```bash
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
``` ```
... and transfer to the test server: `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari` ... and transfer to the test server:
(Let's replace that with a .env.development or .env.staging file.) ```bash
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
```
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.) (Let's replace that with a .env.development or .env.staging file.)
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
* For prod, get on the server and run the correct build: * For prod, get on the server and run the correct build:
@ -76,23 +79,14 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production. * Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
## Tests ## Tests
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions. See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
## Icons ## Icons
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name. To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
## Other ## Other
### Reference Material ### Reference Material
@ -104,7 +98,6 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
* 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/",`
### Kudos ### Kudos
Gifts make the world go 'round! Gifts make the world go 'round!

915
package-lock.json

File diff suppressed because it is too large

2
package.json

@ -125,6 +125,8 @@
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-vue": "^9.32.0",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"markdownlint": "^0.37.4",
"markdownlint-cli": "^0.44.0",
"npm-check-updates": "^17.1.13", "npm-check-updates": "^17.1.13",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.2.5", "prettier": "^3.2.5",

5
src/interfaces/common.ts

@ -19,11 +19,6 @@ export interface ResultWithType {
type: string; type: string;
} }
export interface SuccessResult extends ResultWithType {
type: "success";
response: unknown;
}
export interface ErrorResponse { export interface ErrorResponse {
error?: { error?: {
message?: string; message?: string;

13
src/interfaces/deepLinks.ts

@ -0,0 +1,13 @@
/**
* @file Deep Link Interface Definitions
* @author Matthew Raymer
*
* Defines the core interfaces for the deep linking system.
* These interfaces are used across the deep linking implementation
* to ensure type safety and consistent error handling.
*/
export interface DeepLinkError extends Error {
code: string;
details?: unknown;
}

1
src/interfaces/index.ts

@ -4,3 +4,4 @@ export * from "./common";
export * from "./limits"; export * from "./limits";
export * from "./records"; export * from "./records";
export * from "./user"; export * from "./user";
export * from "./deepLinks";

9
src/main.capacitor.ts

@ -72,9 +72,12 @@ const handleDeepLink = async (data: { url: string }) => {
await deepLinkHandler.handleDeepLink(data.url); await deepLinkHandler.handleDeepLink(data.url);
} catch (error) { } catch (error) {
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true); logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
handleApiError({ handleApiError(
message: error instanceof Error ? error.message : String(error) {
} as AxiosError, "deep-link"); message: error instanceof Error ? error.message : String(error),
} as AxiosError,
"deep-link",
);
} }
}; };

83
src/services/deepLinks.ts

@ -29,13 +29,9 @@
*/ */
import { Router } from "vue-router"; import { Router } from "vue-router";
import { deepLinkSchemas, DeepLinkParams } from "../types/deepLinks"; import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks";
import { logConsoleAndDb } from "../db"; import { logConsoleAndDb } from "../db";
import type { DeepLinkError } from "../interfaces/deepLinks";
interface DeepLinkError extends Error {
code: string;
details?: unknown;
}
export class DeepLinkHandler { export class DeepLinkHandler {
private router: Router; private router: Router;
@ -44,6 +40,39 @@ export class DeepLinkHandler {
this.router = router; this.router = router;
} }
/**
* Parses deep link URL into path, params and query components
*/
private parseDeepLink(url: string) {
const parts = url.split("://");
if (parts.length !== 2) {
throw { code: "INVALID_URL", message: "Invalid URL format" };
}
// Validate base URL structure
baseUrlSchema.parse({
scheme: parts[0],
path: parts[1],
queryParams: {}, // Will be populated below
});
const [path, queryString] = parts[1].split("?");
const [routePath, param] = path.split("/");
const query: Record<string, string> = {};
if (queryString) {
new URLSearchParams(queryString).forEach((value, key) => {
query[key] = value;
});
}
return {
path: routePath,
params: param ? { id: param } : {},
query,
};
}
/** /**
* Processes incoming deep links and routes them appropriately * Processes incoming deep links and routes them appropriately
* @param url The deep link URL to process * @param url The deep link URL to process
@ -51,21 +80,23 @@ export class DeepLinkHandler {
async handleDeepLink(url: string): Promise<void> { async handleDeepLink(url: string): Promise<void> {
try { try {
logConsoleAndDb("[DeepLink] Processing URL: " + url, false); logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
const { path, params, query } = this.parseDeepLink(url); const { path, params, query } = this.parseDeepLink(url);
await this.validateAndRoute(path, params, query); // Ensure params is always a Record<string,string> by converting undefined to empty string
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
);
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) { } catch (error) {
const deepLinkError = error as DeepLinkError; const deepLinkError = error as DeepLinkError;
logConsoleAndDb( logConsoleAndDb(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`, `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
true true,
); );
throw { throw {
code: deepLinkError.code || 'UNKNOWN_ERROR', code: deepLinkError.code || "UNKNOWN_ERROR",
message: deepLinkError.message, message: deepLinkError.message,
details: deepLinkError.details details: deepLinkError.details,
}; };
} }
} }
@ -76,25 +107,25 @@ export class DeepLinkHandler {
private async validateAndRoute( private async validateAndRoute(
path: string, path: string,
params: Record<string, string>, params: Record<string, string>,
query: Record<string, string> query: Record<string, string>,
): Promise<void> { ): Promise<void> {
const routeMap: Record<string, string> = { const routeMap: Record<string, string> = {
claim: 'claim', claim: "claim",
'claim-cert': 'claim-cert', "claim-cert": "claim-cert",
'claim-add-raw': 'claim-add-raw', "claim-add-raw": "claim-add-raw",
'contact-edit': 'contact-edit', "contact-edit": "contact-edit",
'contact-import': 'contact-import', "contact-import": "contact-import",
project: 'project', project: "project",
'invite-one-accept': 'invite-one-accept', "invite-one-accept": "invite-one-accept",
'offer-details': 'offer-details', "offer-details": "offer-details",
'confirm-gift': 'confirm-gift' "confirm-gift": "confirm-gift",
}; };
const routeName = routeMap[path]; const routeName = routeMap[path];
if (!routeName) { if (!routeName) {
throw { throw {
code: 'INVALID_ROUTE', code: "INVALID_ROUTE",
message: `Unsupported route: ${path}` message: `Unsupported route: ${path}`,
}; };
} }
@ -102,13 +133,13 @@ export class DeepLinkHandler {
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
const validatedParams = await schema.parseAsync({ const validatedParams = await schema.parseAsync({
...params, ...params,
...query ...query,
}); });
await this.router.replace({ await this.router.replace({
name: routeName, name: routeName,
params: validatedParams, params: validatedParams,
query query,
}); });
} }
} }

20
src/types/deepLinks.ts

@ -28,46 +28,46 @@
import { z } from "zod"; import { z } from "zod";
// Base URL validation schema // Base URL validation schema
const baseUrlSchema = z.object({ export const baseUrlSchema = z.object({
scheme: z.literal("timesafari"), scheme: z.literal("timesafari"),
path: z.string(), path: z.string(),
queryParams: z.record(z.string()).optional() queryParams: z.record(z.string()).optional(),
}); });
// Parameter validation schemas for each route type // Parameter validation schemas for each route type
export const deepLinkSchemas = { export const deepLinkSchemas = {
claim: z.object({ claim: z.object({
id: z.string().min(1), id: z.string().min(1),
view: z.enum(["details", "certificate", "raw"]).optional() view: z.enum(["details", "certificate", "raw"]).optional(),
}), }),
contact: z.object({ contact: z.object({
did: z.string().regex(/^did:/), did: z.string().regex(/^did:/),
action: z.enum(["edit", "import"]).optional(), action: z.enum(["edit", "import"]).optional(),
jwt: z.string().optional() jwt: z.string().optional(),
}), }),
project: z.object({ project: z.object({
id: z.string().min(1), id: z.string().min(1),
view: z.enum(["details", "edit"]).optional() view: z.enum(["details", "edit"]).optional(),
}), }),
invite: z.object({ invite: z.object({
jwt: z.string().min(1), jwt: z.string().min(1),
type: z.enum(["one", "many"]).optional() type: z.enum(["one", "many"]).optional(),
}), }),
gift: z.object({ gift: z.object({
id: z.string().min(1), id: z.string().min(1),
action: z.enum(["confirm", "details"]).optional() action: z.enum(["confirm", "details"]).optional(),
}), }),
offer: z.object({ offer: z.object({
id: z.string().min(1), id: z.string().min(1),
view: z.enum(["details"]).optional() view: z.enum(["details"]).optional(),
}) }),
}; };
export type DeepLinkParams = { export type DeepLinkParams = {
[K in keyof typeof deepLinkSchemas]: z.infer<typeof deepLinkSchemas[K]>; [K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
}; };

93
web-push.md

@ -37,7 +37,7 @@ possibility of users clicking "don't allow".
Now, to explain what happens in Typescript, we can activate a browser's Now, to explain what happens in Typescript, we can activate a browser's
permission dialogue in this manner: permission dialogue in this manner:
``` ```typescript
function askPermission(): Promise<NotificationPermission> { function askPermission(): Promise<NotificationPermission> {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
const permissionResult = Notification.requestPermission(function(result) { const permissionResult = Notification.requestPermission(function(result) {
@ -59,9 +59,9 @@ function askPermission(): Promise<NotificationPermission> {
The Notification.permission property indicates the permission level for the The Notification.permission property indicates the permission level for the
current session and returns one of the following string values: current session and returns one of the following string values:
'granted': The user has granted permission for notifications. - 'granted': The user has granted permission for notifications.
'denied': The user has denied permission for notifications. - 'denied': The user has denied permission for notifications.
'default': The user has not made a choice yet. - 'default': The user has not made a choice yet.
Once the user has granted permission, the client application registers a service Once the user has granted permission, the client application registers a service
worker using the `ServiceWorkerRegistration` API. worker using the `ServiceWorkerRegistration` API.
@ -77,7 +77,7 @@ subscriptions may be done.
Let's go through the `register` method first: Let's go through the `register` method first:
``` ```javascript
navigator.serviceWorker.register('sw.js', { scope: '/' }) navigator.serviceWorker.register('sw.js', { scope: '/' })
.then(function(registration) { .then(function(registration) {
console.log('Service worker registered successfully:', registration); console.log('Service worker registered successfully:', registration);
@ -108,7 +108,8 @@ Here's a version which can be used for testing locally. Note there can be
caching issues in your browser! Incognito is highly recommended. caching issues in your browser! Incognito is highly recommended.
sw-dev.ts sw-dev.ts
```
```typescript
self.addEventListener('push', function(event: PushEvent) { self.addEventListener('push', function(event: PushEvent) {
console.log('Received a push message', event); console.log('Received a push message', event);
@ -127,9 +128,9 @@ self.addEventListener('push', function(event: PushEvent) {
}); });
``` ```
vue.config.js vue.config.js
```
```javascript
module.exports = { module.exports = {
pwa: { pwa: {
workboxOptions: { workboxOptions: {
@ -159,29 +160,29 @@ security and authenticity for web push notifications in the following ways:
Identifying the Application Server: Identifying the Application Server:
The VAPID key is used to identify the application server that is sending The VAPID key is used to identify the application server that is sending
the push notifications. This ensures that the push notifications are the push notifications. This ensures that the push notifications are
authentic and not sent by a malicious third party. authentic and not sent by a malicious third party.
Encrypting the Messages: Encrypting the Messages:
The VAPID key is used to sign the push notifications sent by the The VAPID key is used to sign the push notifications sent by the
application server, ensuring that they are not tampered with during application server, ensuring that they are not tampered with during
transmission. This provides an additional layer of security and transmission. This provides an additional layer of security and
authenticity for the push notifications. authenticity for the push notifications.
Adding Contact Information: Adding Contact Information:
The VAPID key allows a web application to add contact information to The VAPID key allows a web application to add contact information to
the push messages sent to the browser push service. This enables the the push messages sent to the browser push service. This enables the
push service to contact the application server in case of need or push service to contact the application server in case of need or
provide additional debug information about the push messages. provide additional debug information about the push messages.
Improving Delivery Rates: Improving Delivery Rates:
Using the VAPID key can help improve the overall performance of web push Using the VAPID key can help improve the overall performance of web push
notifications, specifically improving delivery rates. By streamlining the notifications, specifically improving delivery rates. By streamlining the
delivery process, the chance of delivery errors along the way is lessened. 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 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 SERVICE Web Push messages, then the BROWSER makes a subscription request to
@ -189,7 +190,7 @@ PROVIDER which creates and stores a special URL for that BROWSER.
Here's a bit of code describing the above process: Here's a bit of code describing the above process:
``` ```typescript
// b64 is the VAPID // b64 is the VAPID
b64 = 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'; b64 = 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U';
const applicationServerKey = urlBase64ToUint8Array(b64); const applicationServerKey = urlBase64ToUint8Array(b64);
@ -210,7 +211,7 @@ registration.pushManager.subscribe(options)
In this example, the `applicationServerKey` variable contains the VAPID public In this example, the `applicationServerKey` variable contains the VAPID public
key, which is converted to a `Uint8Array` using a function such as this: key, which is converted to a `Uint8Array` using a function such as this:
``` ```typescript
export function toUint8Array(base64String: string, atobFn: typeof atob): Uint8Array { export function toUint8Array(base64String: string, atobFn: typeof atob): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
@ -242,6 +243,7 @@ The subscribe() method returns a `Promise` that resolves to a `PushSubscription`
object containing details of the subscription, such as the endpoint URL and the object containing details of the subscription, such as the endpoint URL and the
public key. The returned data would have a form like this: public key. The returned data would have a form like this:
```json
{ {
"endpoint": "https://some.pushservice.com/some/unique/identifier", "endpoint": "https://some.pushservice.com/some/unique/identifier",
"expirationTime": null, "expirationTime": null,
@ -250,6 +252,7 @@ public key. The returned data would have a form like this:
"auth": "some_other_base64_encoded_string" "auth": "some_other_base64_encoded_string"
} }
} }
```
endpoint: A string representing the endpoint URL for the push service. This 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 URL is essentially the push service address to which the push message would
@ -273,7 +276,7 @@ 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 Just to remind us that in our service worker our code for receiving messages
will look something like this: will look something like this:
``` ```typescript
self.addEventListener('push', function(event: PushEvent) { self.addEventListener('push', function(event: PushEvent) {
console.log('Received a push message', event); console.log('Received a push message', event);
@ -292,7 +295,6 @@ self.addEventListener('push', function(event: PushEvent) {
}); });
``` ```
Now to address the issue of receiving notification messages on mobile devices. 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 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 open, except in the cases of Chrome and Firefox mobile BROWSERS. In iOS, the
@ -300,13 +302,13 @@ 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 permissions must be explicitly granted that allow the application to receive
push notifications. Further, with an iOS device the user must enable wake on 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 notification to have their device light-up when it receives a notification
(https://support.apple.com/enus/HT208081). (<https://support.apple.com/enus/HT208081>).
So what about #4? - The INTERMEDIARY. Well, It is possible under very special 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 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 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 customized version of the Android Operating System.) There are open source
IMTERMEDIARY products such as UnifiedPush (https://unifiedpush.org/) which can 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 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. your own custom Web Push PROVIDER. Apple will never allow anyone to do that.
Apple has none of its own. Apple has none of its own.
@ -315,8 +317,7 @@ 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 and FCM (or iOS). Services that mash up various Push notification services (like
OneSignal) can perform in the role of such proxies. OneSignal) can perform in the role of such proxies.
#4 -The INTERMEDIARY- doesn't appear to be anything we should be spending our # 4 -The INTERMEDIARY- doesn't appear to be anything we should be spending our time on
time on.
A BROWSER may also remove a subscription. In order to remove a subscription, A BROWSER may also remove a subscription. In order to remove a subscription,
the registration record must be retrieved from the serviceWorker using the registration record must be retrieved from the serviceWorker using
@ -326,8 +327,7 @@ subscription object, you may call the `unsubscribe` method. `unsubscribe` is
asynchronnous and returns a boolean true if it is successful in removing the asynchronnous and returns a boolean true if it is successful in removing the
subscription and false if not. subscription and false if not.
```typescript
```
async function unsubscribeFromPush() { async function unsubscribeFromPush() {
// Check if the browser supports service workers // Check if the browser supports service workers
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
@ -362,30 +362,34 @@ unsubscribeFromPush().catch((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. 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.
# NOTIFICATION DIALOG WORKFLOW # NOTIFICATION DIALOG WORKFLOW
## ON APP FIRST-LAUNCH: ## ON APP FIRST-LAUNCH
The user is periodically presented with the notification permission dialog that asks them if they want to turn on notifications. User is given 3 choices: The user is periodically presented with the notification permission dialog that asks them if they want to turn on notifications. User is given 3 choices:
- "Turn on Notifications": triggers the browser's own notification permission prompt. - "Turn on Notifications": triggers the browser's own notification permission prompt.
- "Maybe Later": dismisses the dialog, to reappear at a later instance. (The next time the user launches the app? After X amount of days? A combination of both?) - "Maybe Later": dismisses the dialog, to reappear at a later instance. (The next time the user launches the app? After X amount of days? A combination of both?)
- "Never": dismisses the dialog; app remembers to not automatically present the dialog again. - "Never": dismisses the dialog; app remembers to not automatically present the dialog again.
## IF THE USER CHOOSES "NEVER": ## IF THE USER CHOOSES "NEVER"
The dialog can still be accessed via the Notifications toggle switch in `AccountViewView` (which also tells the user if notifications are turned on or off). The dialog can still be accessed via the Notifications toggle switch in `AccountViewView` (which also tells the user if notifications are turned on or off).
## TO TEMPORARILY MUTE NOTIFICATIONS: ## TO TEMPORARILY MUTE NOTIFICATIONS
While notifications are turned on, the user can tap on the Mute Notifications toggle switch in `AccountViewView` (visible only when notifications are turned on) to trigger the Mute Notifications Dialog. User is given the following choices: While notifications are turned on, the user can tap on the Mute Notifications toggle switch in `AccountViewView` (visible only when notifications are turned on) to trigger the Mute Notifications Dialog. User is given the following choices:
- Several "Mute for X Hour/s" buttons to temporarily mute notifications. - Several "Mute for X Hour/s" buttons to temporarily mute notifications.
- "Mute until I turn it back on" button to indefinitely mute notifications. - "Mute until I turn it back on" button to indefinitely mute notifications.
- "Cancel" to make no changes and dismiss the dialog. - "Cancel" to make no changes and dismiss the dialog.
## TO UNMUTE NOTIFICATIONS: ## TO UNMUTE NOTIFICATIONS
Simply tap on the Mute Notifications toggle switch in `AccountViewView` to immediately unmute notifications. No dialog needed. Simply tap on the Mute Notifications toggle switch in `AccountViewView` to immediately unmute notifications. No dialog needed.
## TO TURN OFF NOTIFICATIONS: ## TO TURN OFF NOTIFICATIONS
While notifications are turned on, the user can tap on the App Notifications toggle switch in `AccountViewView` to trigger the Turn Off Notifications Dialog. User is given the following choices: While notifications are turned on, the user can tap on the App Notifications toggle switch in `AccountViewView` to trigger the Turn Off Notifications Dialog. User is given the following choices:
- "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on). - "Turn off Notifications" to fully turn them off (which means the user will need to go through the dialogs agains to turn them back on).
@ -393,29 +397,28 @@ While notifications are turned on, the user can tap on the App Notifications tog
# NOTIFICATION STATES # NOTIFICATION STATES
* Unpermissioned. Push server cannot send notifications to the user because it does not have permission. - Unpermissioned. Push server cannot send notifications to the user because it does not have permission.
This may be the same as when the user gave permission in the past but has since revoked it at the OS or browser This may be the same as when the user gave permission in the past but has since revoked it at the OS or browser
level, outside the app. (User can change to Permissioned when the user gives permission.) level, outside the app. (User can change to Permissioned when the user gives permission.)
* Permissioned. (User can change to Unpermissioned via the OS or browser settings.) - Permissioned. (User can change to Unpermissioned via the OS or browser settings.)
* Active. (User can change to Muted when the user mutes notifications.) - Active. (User can change to Muted when the user mutes notifications.)
* Muted. (User can change to Active when the user toggles it.) - Muted. (User can change to Active when the user toggles it.)
(Turning mute off automatically after some amount of time is not planned in version 1.) (Turning mute off automatically after some amount of time is not planned in version 1.)
# TROUBLESHOOTING # TROUBLESHOOTING
## Desktop ## Desktop
#### Firefox ### Firefox
Go to `about:debugging` and click on `Inspect` for the service worker. Go to `about:debugging` and click on `Inspect` for the service worker.
#### Chrome ### Chrome
Go to `chrome://inspect/#service-workers` and click on `Inspect` for the service worker. Go to `chrome://inspect/#service-workers` and click on `Inspect` for the service worker.
## Mobile ## Mobile
#### Android ### Android
#### iOS ### iOS

Loading…
Cancel
Save