feat: add dev startup script, fix push notifications, and scope test sends to device
- pnpm dev:local/dev:reset: orchestrate DB container, migrations, seed, and Next.js dev server in one command; Caddy snippet + docs for HTTPS via dev.ginnoir.com - Fix dev login on HTTPS: set both authjs.session-token and __Secure-authjs.session-token so Auth.js finds the session regardless of cookie name resolution - Suppress hydration mismatch on <html> caused by pre-paint script changing data-nav before React hydrates - VAPID startup warning if keys not configured; remove dead NEXT_PUBLIC_VAPID_PUBLIC_KEY var - PushOptIn: hydrate subscription state on mount; reuse existing subscription on iOS to avoid redundant prompts - sendPushToEndpoint: new function to send to a single device endpoint - sendTestNotification: scoped to the calling device's endpoint (ownership-verified) instead of all user subscriptions
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useState, useTransition, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
subscribeToPush,
|
||||
@@ -23,6 +23,20 @@ export function PushOptIn({ vapidKey }: { vapidKey: string }) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [testSent, setTestSent] = useState(false);
|
||||
|
||||
// Hydrate subscription state from the browser on mount so the UI reflects
|
||||
// reality even when the user navigates away and returns to Settings.
|
||||
useEffect(() => {
|
||||
if (!vapidKey || !("serviceWorker" in navigator) || !("PushManager" in window)) return;
|
||||
navigator.serviceWorker.ready.then((reg) =>
|
||||
reg.pushManager.getSubscription().then((existing) => {
|
||||
if (existing) {
|
||||
setEndpoint(existing.endpoint);
|
||||
setStatus("subscribed");
|
||||
}
|
||||
}),
|
||||
);
|
||||
}, [vapidKey]);
|
||||
|
||||
if (!vapidKey) return null;
|
||||
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
|
||||
return (
|
||||
@@ -35,10 +49,15 @@ export function PushOptIn({ vapidKey }: { vapidKey: string }) {
|
||||
async function subscribe() {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const sub = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidKey),
|
||||
});
|
||||
// Reuse an existing browser subscription rather than calling subscribe()
|
||||
// again — avoids redundant prompts and inconsistent behaviour on iOS.
|
||||
const existing = await registration.pushManager.getSubscription();
|
||||
const sub =
|
||||
existing ??
|
||||
(await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidKey),
|
||||
}));
|
||||
const json = sub.toJSON() as { endpoint: string; keys: { p256dh: string; auth: string } };
|
||||
startTransition(async () => {
|
||||
await subscribeToPush(json, navigator.userAgent);
|
||||
@@ -63,8 +82,9 @@ export function PushOptIn({ vapidKey }: { vapidKey: string }) {
|
||||
}
|
||||
|
||||
function sendTest() {
|
||||
if (!endpoint) return;
|
||||
startTransition(async () => {
|
||||
await sendTestNotification();
|
||||
await sendTestNotification(endpoint);
|
||||
setTestSent(true);
|
||||
setTimeout(() => setTestSent(false), 3000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user