diff --git a/src/components/AddToCartButton.js b/src/components/AddToCartButton.js index ed09909..6683226 100644 --- a/src/components/AddToCartButton.js +++ b/src/components/AddToCartButton.js @@ -15,6 +15,8 @@ import NotificationsActiveIcon from "@mui/icons-material/NotificationsActive"; import CircularProgress from "@mui/material/CircularProgress"; import { withI18n } from "../i18n/withTranslation.js"; import { + PUSH_SUBSCRIPTIONS_CHANGED_EVENT, + emitPushSubscriptionsChanged, isPushApiSupported, fetchPushConfiguration, registerPushServiceWorker, @@ -109,9 +111,10 @@ class AddToCartButton extends Component { this.setState({ pushSubscribed: false, pushBusy: false }); return; } - const res = await articlePushUnsubscribe(subscription.endpoint); + const res = await articlePushUnsubscribe(subscription.endpoint, kArtikel); if (parseSuccess(res)) { this.setState({ pushSubscribed: false }); + emitPushSubscriptionsChanged(); } else { this.setState({ pushError: @@ -146,6 +149,7 @@ class AddToCartButton extends Component { const res = await articlePushSubscribe(kArtikel, subscription); if (parseSuccess(res)) { this.setState({ pushSubscribed: true }); + emitPushSubscriptionsChanged(); } else { this.setState({ pushError: @@ -174,7 +178,14 @@ class AddToCartButton extends Component { if (this.state.quantity !== newQuantity) this.setState({ quantity: newQuantity }); }; + this.onPushSubscriptionsChanged = () => { + this.refreshIncomingPushStatus(); + }; window.addEventListener("cart", this.cart); + window.addEventListener( + PUSH_SUBSCRIPTIONS_CHANGED_EVENT, + this.onPushSubscriptionsChanged + ); this.refreshIncomingPushStatus(); } @@ -190,6 +201,10 @@ class AddToCartButton extends Component { componentWillUnmount() { window.removeEventListener("cart", this.cart); + window.removeEventListener( + PUSH_SUBSCRIPTIONS_CHANGED_EVENT, + this.onPushSubscriptionsChanged + ); } handleIncrement = () => { diff --git a/src/components/ProductFilters.js b/src/components/ProductFilters.js index 632d42d..4a1eef1 100644 --- a/src/components/ProductFilters.js +++ b/src/components/ProductFilters.js @@ -12,6 +12,8 @@ import { useParams, useSearchParams, useNavigate, useLocation } from 'react-rout import { setSessionSetting, removeSessionSetting, clearAllSessionSettings } from '../utils/sessionStorage.js'; import { withI18n } from '../i18n/withTranslation.js'; import { + PUSH_SUBSCRIPTIONS_CHANGED_EVENT, + emitPushSubscriptionsChanged, isPushApiSupported, fetchPushConfiguration, registerPushServiceWorker, @@ -68,14 +70,25 @@ class ProductFilters extends Component { } componentDidMount() { + this.onPushSubscriptionsChanged = () => { + this.refreshCategoryPushStatus(); + }; this.adjustPaperHeight(); window.addEventListener('resize', this.adjustPaperHeight); + window.addEventListener( + PUSH_SUBSCRIPTIONS_CHANGED_EVENT, + this.onPushSubscriptionsChanged + ); this._loadManufacturerImages(); this.refreshCategoryPushStatus(); } componentWillUnmount() { window.removeEventListener('resize', this.adjustPaperHeight); + window.removeEventListener( + PUSH_SUBSCRIPTIONS_CHANGED_EVENT, + this.onPushSubscriptionsChanged + ); this._manufacturerImageUrls.forEach(url => URL.revokeObjectURL(url)); } @@ -204,9 +217,10 @@ class ProductFilters extends Component { this.setState({ pushSubscribed: false, pushBusy: false }); return; } - const res = await categoryPushUnsubscribe(subscription.endpoint); + const res = await categoryPushUnsubscribe(subscription.endpoint, kKat); if (parseSuccess(res)) { this.setState({ pushSubscribed: false }); + emitPushSubscriptionsChanged(); } else { this.setState({ pushError: @@ -237,6 +251,7 @@ class ProductFilters extends Component { const res = await categoryPushSubscribe(kKat, subscription); if (parseSuccess(res)) { this.setState({ pushSubscribed: true }); + emitPushSubscriptionsChanged(); } else { this.setState({ pushError: diff --git a/src/utils/articlePush.js b/src/utils/articlePush.js index 8a9e64f..dadd850 100644 --- a/src/utils/articlePush.js +++ b/src/utils/articlePush.js @@ -2,6 +2,15 @@ * Web Push helpers for article notifications (VAPID + backend API). */ +/** Fired after any article/category push subscribe or unsubscribe so all UIs re-query status. */ +export const PUSH_SUBSCRIPTIONS_CHANGED_EVENT = "growheads-push-subscriptions-changed"; + +export function emitPushSubscriptionsChanged() { + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent(PUSH_SUBSCRIPTIONS_CHANGED_EVENT)); + } +} + export const ARTICLE_PUSH_SW_PATH = "/api/check/push/service-worker"; export const ARTICLE_PUSH_VAPID_URL = "/api/check/push/vapid-public-key"; @@ -89,13 +98,28 @@ export async function articlePushSubscribe(kArtikel, subscription) { } /** + * POST /api/article/push/unsubscribe — body: `{ endpoint }` or `{ subscription: { endpoint } }` (pickup-style). + * Optional `kArtikel` (number or numeric string): if it parses to a finite id > 0, only that row is removed. + * Otherwise legacy: server clears all push rows for that endpoint (article + category + pickup). + * * @param {string} endpoint + * @param {number|string} [kArtikel] */ -export async function articlePushUnsubscribe(endpoint) { +export async function articlePushUnsubscribe(endpoint, kArtikel) { + const parsed = + kArtikel != null && kArtikel !== "" + ? typeof kArtikel === "number" + ? kArtikel + : parseInt(String(kArtikel), 10) + : NaN; + const body = + Number.isFinite(parsed) && parsed > 0 + ? { endpoint, kArtikel: parsed } + : { endpoint }; const res = await fetch("/api/article/push/unsubscribe", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ endpoint }), + body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); return { ...data, httpOk: res.ok }; diff --git a/src/utils/categoryPush.js b/src/utils/categoryPush.js index 05210b9..ad9f323 100644 --- a/src/utils/categoryPush.js +++ b/src/utils/categoryPush.js @@ -3,6 +3,8 @@ */ export { + PUSH_SUBSCRIPTIONS_CHANGED_EVENT, + emitPushSubscriptionsChanged, fetchPushConfiguration, registerPushServiceWorker, ensurePushSubscription, @@ -44,13 +46,26 @@ export async function categoryPushSubscribe(kKategorie, subscription) { } /** + * POST /api/category/push/unsubscribe — same contract as article: optional `kKategorie` for scoped delete. + * * @param {string} endpoint + * @param {number|string} [kKategorie] */ -export async function categoryPushUnsubscribe(endpoint) { +export async function categoryPushUnsubscribe(endpoint, kKategorie) { + const parsed = + kKategorie != null && kKategorie !== "" + ? typeof kKategorie === "number" + ? kKategorie + : parseInt(String(kKategorie), 10) + : NaN; + const body = + Number.isFinite(parsed) && parsed > 0 + ? { endpoint, kKategorie: parsed } + : { endpoint }; const res = await fetch("/api/category/push/unsubscribe", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ endpoint }), + body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); return { ...data, httpOk: res.ok };