CI / ci (push) Canceled after 0s
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.
106 lines
3.7 KiB
TypeScript
106 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
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
|
|
* installs and sits `waiting` rather than taking over immediately, so an
|
|
* open tab isn't silently switched to a new app version mid-session. This
|
|
* component detects that waiting worker and lets the user opt into the
|
|
* refresh instead.
|
|
*/
|
|
export function ServiceWorkerManager() {
|
|
const [waitingWorker, setWaitingWorker] = useState<ServiceWorker | null>(null);
|
|
const [dismissed, setDismissed] = useState(false);
|
|
|
|
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) => {
|
|
// An update was already downloaded (e.g. by another tab) before this page loaded.
|
|
if (registration.waiting && navigator.serviceWorker.controller) {
|
|
setWaitingWorker(registration.waiting);
|
|
}
|
|
|
|
registration.addEventListener("updatefound", () => {
|
|
const installing = registration.installing;
|
|
if (!installing) return;
|
|
installing.addEventListener("statechange", () => {
|
|
// Only a real update — an installing worker with no existing
|
|
// controller is just this tab's first-ever SW install, not an update.
|
|
if (installing.state === "installed" && navigator.serviceWorker.controller) {
|
|
setWaitingWorker(installing);
|
|
}
|
|
});
|
|
});
|
|
})
|
|
.catch(() => {
|
|
// Best-effort — offline/install/update support degrades gracefully without it.
|
|
});
|
|
|
|
let reloaded = false;
|
|
const onControllerChange = () => {
|
|
if (reloaded) return;
|
|
reloaded = true;
|
|
window.location.reload();
|
|
};
|
|
navigator.serviceWorker.addEventListener("controllerchange", onControllerChange);
|
|
return () => {
|
|
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
navigator.serviceWorker.removeEventListener("controllerchange", onControllerChange);
|
|
};
|
|
}, []);
|
|
|
|
if (!waitingWorker || dismissed) return null;
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
className="rounded-md bg-ink fixed inset-x-6 top-4 z-50 mx-auto flex max-w-sm items-center justify-between gap-3 px-5 py-3 text-sm text-white shadow-xl"
|
|
>
|
|
<span>A new version is available.</span>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
className="font-bold whitespace-nowrap underline"
|
|
onClick={() => waitingWorker.postMessage({ type: "SKIP_WAITING" })}
|
|
>
|
|
Refresh
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="Dismiss"
|
|
className="text-white/60"
|
|
onClick={() => setDismissed(true)}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|