Policies section (business-details-form.tsx) Late Arrival Policy, Booking Terms and Privacy Policy URL are gone. The section now has Cancellation Policy plus the Terms & Privacy name and email dropdowns. Their database columns stay, so nothing is lost. Saving no longer touches them. Email and push only One switch, IN_APP_NOTIFICATIONS_VISIBLE in in-app.ts, is set to false and controls everything below. Bell icon: gone for customers, staff and owners, including the unread badge. Notification pages: /notifications, /staff/notifications and /owner/notifications now return 404. Settings: Settings → Notifications shows only Email and Push toggles. Testing: I checked all of this in a local test app as the owner.
This commit is contained in:
19 files changed
+263
-40
No files matched your search
@@ -0,0 +1,90 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { runInNewContext } from "node:vm";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
type Handler = (event: Record<string, unknown>) => void;
|
||||
|
||||
/** Loads the real public/sw.js into a fake service-worker global and returns its event handlers. */
|
||||
function loadServiceWorker(options: {
|
||||
shownNotifications: number;
|
||||
appVisible: boolean;
|
||||
badging?: boolean;
|
||||
}) {
|
||||
const handlers: Record<string, Handler> = {};
|
||||
const setAppBadge = vi.fn().mockResolvedValue(undefined);
|
||||
const clearAppBadge = vi.fn().mockResolvedValue(undefined);
|
||||
const showNotification = vi.fn().mockResolvedValue(undefined);
|
||||
const shown = { count: options.shownNotifications };
|
||||
|
||||
const self: Record<string, unknown> = {
|
||||
addEventListener: (type: string, handler: Handler) => {
|
||||
handlers[type] = handler;
|
||||
},
|
||||
navigator: options.badging === false ? {} : { setAppBadge, clearAppBadge },
|
||||
registration: {
|
||||
showNotification,
|
||||
getNotifications: async () => Array.from({ length: shown.count }, () => ({})),
|
||||
},
|
||||
clients: {
|
||||
matchAll: async () => [
|
||||
{ visibilityState: options.appVisible ? "visible" : "hidden", url: "/" },
|
||||
],
|
||||
openWindow: vi.fn(),
|
||||
},
|
||||
location: { origin: "https://example.test" },
|
||||
};
|
||||
self.self = self;
|
||||
|
||||
const source = readFileSync(join(process.cwd(), "public/sw.js"), "utf8");
|
||||
runInNewContext(source, { ...self, self, caches: {}, URL, Response, Promise });
|
||||
|
||||
const dispatch = async (type: string, event: Record<string, unknown>) => {
|
||||
const pending: Promise<unknown>[] = [];
|
||||
handlers[type]!({ ...event, waitUntil: (p: Promise<unknown>) => pending.push(p) });
|
||||
await Promise.all(pending);
|
||||
};
|
||||
|
||||
return { dispatch, setAppBadge, clearAppBadge, showNotification, shown };
|
||||
}
|
||||
|
||||
const pushEvent = {
|
||||
data: {
|
||||
json: () => ({ title: "Booking rescheduled", body: "Moved to 14:00", url: "/appointments" }),
|
||||
},
|
||||
};
|
||||
|
||||
describe("service worker app-icon badge", () => {
|
||||
it("badges the icon with the number of notifications still showing after a push", async () => {
|
||||
const sw = loadServiceWorker({ shownNotifications: 3, appVisible: false });
|
||||
await sw.dispatch("push", pushEvent);
|
||||
|
||||
expect(sw.showNotification).toHaveBeenCalledOnce();
|
||||
expect(sw.setAppBadge).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it("does not badge the icon while the app is open in front of the person", async () => {
|
||||
const sw = loadServiceWorker({ shownNotifications: 1, appVisible: true });
|
||||
await sw.dispatch("push", pushEvent);
|
||||
|
||||
expect(sw.showNotification).toHaveBeenCalledOnce();
|
||||
expect(sw.setAppBadge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates the badge when a notification is tapped, clearing it once none are left", async () => {
|
||||
const sw = loadServiceWorker({ shownNotifications: 0, appVisible: false });
|
||||
await sw.dispatch("notificationclick", {
|
||||
notification: { close: vi.fn(), data: { url: "/appointments" } },
|
||||
});
|
||||
|
||||
expect(sw.clearAppBadge).toHaveBeenCalledOnce();
|
||||
expect(sw.setAppBadge).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still shows the notification where the Badging API is unavailable", async () => {
|
||||
const sw = loadServiceWorker({ shownNotifications: 2, appVisible: false, badging: false });
|
||||
await sw.dispatch("push", pushEvent);
|
||||
|
||||
expect(sw.showNotification).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* The app icon's badge number counts push notifications still sitting in
|
||||
* the notification shelf (see updateAppBadge in public/sw.js). Opening the
|
||||
* app is the "I've seen it" signal, so clear both together — otherwise the
|
||||
* next push would count notifications the person already dealt with.
|
||||
*/
|
||||
function clearAppBadgeAndNotifications() {
|
||||
const badging = navigator as Navigator & { clearAppBadge?: () => Promise<void> };
|
||||
badging.clearAppBadge?.().catch(() => {});
|
||||
navigator.serviceWorker?.ready
|
||||
.then((registration) => registration.getNotifications())
|
||||
.then((notifications) => notifications.forEach((notification) => notification.close()))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the service worker once, app-wide, and surfaces the
|
||||
* update-available handshake public/sw.js sets up: a new SW version
|
||||
@@ -17,6 +32,12 @@ export function ServiceWorkerManager() {
|
||||
useEffect(() => {
|
||||
if (!("serviceWorker" in navigator)) return;
|
||||
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") clearAppBadgeAndNotifications();
|
||||
};
|
||||
onVisibilityChange();
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
@@ -48,7 +69,10 @@ export function ServiceWorkerManager() {
|
||||
window.location.reload();
|
||||
};
|
||||
navigator.serviceWorker.addEventListener("controllerchange", onControllerChange);
|
||||
return () => navigator.serviceWorker.removeEventListener("controllerchange", onControllerChange);
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
navigator.serviceWorker.removeEventListener("controllerchange", onControllerChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!waitingWorker || dismissed) return null;
|
||||
|
||||
Reference in new issue
Block a user