mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
chore: merge upstream (#2024)
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import useSettings from '@app/hooks/useSettings';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import { verifyAndResubscribePushSubscription } from '@app/utils/pushSubscriptionHelpers';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const ServiceWorkerSetup = () => {
|
||||
const { user } = useUser();
|
||||
const { currentSettings } = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
if ('serviceWorker' in navigator && user?.id) {
|
||||
navigator.serviceWorker
|
||||
@@ -14,12 +18,53 @@ const ServiceWorkerSetup = () => {
|
||||
'[SW] Registration successful, scope is:',
|
||||
registration.scope
|
||||
);
|
||||
|
||||
const pushNotificationsEnabled =
|
||||
localStorage.getItem('pushNotificationsEnabled') === 'true';
|
||||
|
||||
// Reset the notifications flag if permissions were revoked
|
||||
if (
|
||||
Notification.permission !== 'granted' &&
|
||||
pushNotificationsEnabled
|
||||
) {
|
||||
localStorage.setItem('pushNotificationsEnabled', 'false');
|
||||
console.warn(
|
||||
'[SW] Push permissions not granted — skipping resubscribe'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Bypass resubscribing if we have manually disabled push notifications
|
||||
if (!pushNotificationsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
|
||||
console.log(
|
||||
'[SW] Existing push subscription:',
|
||||
subscription?.endpoint
|
||||
);
|
||||
|
||||
const verified = await verifyAndResubscribePushSubscription(
|
||||
user.id,
|
||||
currentSettings
|
||||
);
|
||||
|
||||
if (verified) {
|
||||
console.log('[SW] Push subscription verified or refreshed.');
|
||||
} else {
|
||||
console.warn(
|
||||
'[SW] Push subscription verification failed or not available.'
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log('[SW] Service worker registration failed, error:', error);
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
}, [currentSettings, user]);
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import Button from '@app/components/Common/Button';
|
||||
import ConfirmButton from '@app/components/Common/ConfirmButton';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
ComputerDesktopIcon,
|
||||
DevicePhoneMobileIcon,
|
||||
LockClosedIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
interface DeviceItemProps {
|
||||
disablePushNotifications: (p256dh: string) => void;
|
||||
deletePushSubscriptionFromBackend: (endpoint: string) => void;
|
||||
device: {
|
||||
endpoint: string;
|
||||
p256dh: string;
|
||||
@@ -18,6 +20,7 @@ interface DeviceItemProps {
|
||||
userAgent: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
subEndpoint: string | null;
|
||||
}
|
||||
|
||||
const messages = defineMessages(
|
||||
@@ -28,10 +31,15 @@ const messages = defineMessages(
|
||||
engine: 'Engine',
|
||||
deletesubscription: 'Delete Subscription',
|
||||
unknown: 'Unknown',
|
||||
activesubscription: 'Active Subscription',
|
||||
}
|
||||
);
|
||||
|
||||
const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
|
||||
const DeviceItem = ({
|
||||
deletePushSubscriptionFromBackend,
|
||||
device,
|
||||
subEndpoint,
|
||||
}: DeviceItemProps) => {
|
||||
const intl = useIntl();
|
||||
const parsedUserAgent = UAParser(device.userAgent);
|
||||
|
||||
@@ -91,14 +99,21 @@ const DeviceItem = ({ disablePushNotifications, device }: DeviceItemProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="z-10 mt-4 flex w-full flex-col justify-center space-y-2 pl-4 pr-4 xl:mt-0 xl:w-96 xl:items-end xl:pl-0">
|
||||
<ConfirmButton
|
||||
onClick={() => disablePushNotifications(device.endpoint)}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deletesubscription)}</span>
|
||||
</ConfirmButton>
|
||||
{subEndpoint === device.endpoint ? (
|
||||
<Button buttonType="primary" className="w-full" disabled>
|
||||
<LockClosedIcon />{' '}
|
||||
<span>{intl.formatMessage(messages.activesubscription)}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<ConfirmButton
|
||||
onClick={() => deletePushSubscriptionFromBackend(device.endpoint)}
|
||||
confirmText={intl.formatMessage(globalMessages.areyousure)}
|
||||
className="w-full"
|
||||
>
|
||||
<TrashIcon />
|
||||
<span>{intl.formatMessage(messages.deletesubscription)}</span>
|
||||
</ConfirmButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,17 +9,22 @@ import useSettings from '@app/hooks/useSettings';
|
||||
import { useUser } from '@app/hooks/useUser';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import {
|
||||
getPushSubscription,
|
||||
subscribeToPushNotifications,
|
||||
unsubscribeToPushNotifications,
|
||||
verifyPushSubscription,
|
||||
} from '@app/utils/pushSubscriptionHelpers';
|
||||
import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
CloudArrowDownIcon,
|
||||
CloudArrowUpIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import type { UserPushSubscription } from '@server/entity/UserPushSubscription';
|
||||
import type { UserSettingsNotificationsResponse } from '@server/interfaces/api/userSettingsInterfaces';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik } from 'formik';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
@@ -53,6 +58,7 @@ const UserWebPushSettings = () => {
|
||||
const { user } = useUser({ id: Number(router.query.userId) });
|
||||
const { currentSettings } = useSettings();
|
||||
const [webPushEnabled, setWebPushEnabled] = useState(false);
|
||||
const [subEndpoint, setSubEndpoint] = useState<string | null>(null);
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
@@ -72,141 +78,122 @@ const UserWebPushSettings = () => {
|
||||
|
||||
// Subscribes to the push manager
|
||||
// Will only add to the database if subscribing for the first time
|
||||
const enablePushNotifications = () => {
|
||||
if ('serviceWorker' in navigator && user?.id) {
|
||||
navigator.serviceWorker
|
||||
.getRegistration('/sw.js')
|
||||
.then(async (registration) => {
|
||||
if (currentSettings.enablePushRegistration) {
|
||||
const sub = await registration?.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: currentSettings.vapidPublic,
|
||||
});
|
||||
const parsedSub = JSON.parse(JSON.stringify(sub));
|
||||
const enablePushNotifications = async () => {
|
||||
try {
|
||||
const isSubscribed = await subscribeToPushNotifications(
|
||||
user?.id,
|
||||
currentSettings
|
||||
);
|
||||
|
||||
if (parsedSub.keys.p256dh && parsedSub.keys.auth) {
|
||||
await axios.post('/api/v1/user/registerPushSubscription', {
|
||||
endpoint: parsedSub.endpoint,
|
||||
p256dh: parsedSub.keys.p256dh,
|
||||
auth: parsedSub.keys.auth,
|
||||
userAgent: navigator.userAgent,
|
||||
});
|
||||
setWebPushEnabled(true);
|
||||
addToast(intl.formatMessage(messages.webpushhasbeenenabled), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
addToast(intl.formatMessage(messages.enablingwebpusherror), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
})
|
||||
.finally(function () {
|
||||
revalidateDevices();
|
||||
if (isSubscribed) {
|
||||
localStorage.setItem('pushNotificationsEnabled', 'true');
|
||||
setWebPushEnabled(true);
|
||||
addToast(intl.formatMessage(messages.webpushhasbeenenabled), {
|
||||
appearance: 'success',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} else {
|
||||
throw new Error('Subscription failed');
|
||||
}
|
||||
} catch (error) {
|
||||
addToast(intl.formatMessage(messages.enablingwebpusherror), {
|
||||
appearance: 'error',
|
||||
autoDismiss: true,
|
||||
});
|
||||
} finally {
|
||||
revalidateDevices();
|
||||
}
|
||||
};
|
||||
|
||||
// Unsubscribes from the push manager
|
||||
// Deletes/disables corresponding push subscription from database
|
||||
const disablePushNotifications = async (endpoint?: string) => {
|
||||
if ('serviceWorker' in navigator && user?.id) {
|
||||
navigator.serviceWorker.getRegistration('/sw.js').then((registration) => {
|
||||
registration?.pushManager
|
||||
.getSubscription()
|
||||
.then(async (subscription) => {
|
||||
const parsedSub = JSON.parse(JSON.stringify(subscription));
|
||||
try {
|
||||
await unsubscribeToPushNotifications(user?.id, endpoint);
|
||||
|
||||
await axios.delete(
|
||||
`/api/v1/user/${user.id}/pushSubscription/${encodeURIComponent(
|
||||
endpoint ?? parsedSub.endpoint
|
||||
)}`
|
||||
);
|
||||
|
||||
if (
|
||||
subscription &&
|
||||
(endpoint === parsedSub.endpoint || !endpoint)
|
||||
) {
|
||||
subscription.unsubscribe();
|
||||
setWebPushEnabled(false);
|
||||
}
|
||||
addToast(
|
||||
intl.formatMessage(
|
||||
endpoint
|
||||
? messages.subscriptiondeleted
|
||||
: messages.webpushhasbeendisabled
|
||||
),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'success',
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(function () {
|
||||
addToast(
|
||||
intl.formatMessage(
|
||||
endpoint
|
||||
? messages.subscriptiondeleteerror
|
||||
: messages.disablingwebpusherror
|
||||
),
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
}
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
revalidateDevices();
|
||||
});
|
||||
localStorage.setItem('pushNotificationsEnabled', 'false');
|
||||
setWebPushEnabled(false);
|
||||
addToast(intl.formatMessage(messages.webpushhasbeendisabled), {
|
||||
autoDismiss: true,
|
||||
appearance: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
addToast(intl.formatMessage(messages.disablingwebpusherror), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
revalidateDevices();
|
||||
}
|
||||
};
|
||||
|
||||
// Checks our current subscription on page load
|
||||
// Will set the web push state to true if subscribed
|
||||
useEffect(() => {
|
||||
if ('serviceWorker' in navigator && user?.id) {
|
||||
navigator.serviceWorker
|
||||
.getRegistration('/sw.js')
|
||||
.then(async (registration) => {
|
||||
await registration?.pushManager
|
||||
.getSubscription()
|
||||
.then(async (subscription) => {
|
||||
if (subscription) {
|
||||
const parsedKey = JSON.parse(JSON.stringify(subscription));
|
||||
const currentUserPushSub =
|
||||
await axios.get<UserPushSubscription>(
|
||||
`/api/v1/user/${
|
||||
user.id
|
||||
}/pushSubscription/${encodeURIComponent(
|
||||
parsedKey.endpoint
|
||||
)}`
|
||||
);
|
||||
const deletePushSubscriptionFromBackend = async (endpoint: string) => {
|
||||
try {
|
||||
await axios.delete(
|
||||
`/api/v1/user/${user?.id}/pushSubscription/${encodeURIComponent(
|
||||
endpoint
|
||||
)}`
|
||||
);
|
||||
|
||||
if (currentUserPushSub.data.endpoint !== parsedKey.endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
setWebPushEnabled(true);
|
||||
} else {
|
||||
setWebPushEnabled(false);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
setWebPushEnabled(false);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'[SW] Failure retrieving push manager subscription, error:',
|
||||
error
|
||||
);
|
||||
});
|
||||
addToast(intl.formatMessage(messages.subscriptiondeleted), {
|
||||
autoDismiss: true,
|
||||
appearance: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
addToast(intl.formatMessage(messages.subscriptiondeleteerror), {
|
||||
autoDismiss: true,
|
||||
appearance: 'error',
|
||||
});
|
||||
} finally {
|
||||
revalidateDevices();
|
||||
}
|
||||
}, [user?.id]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const verifyWebPush = async () => {
|
||||
const enabled = await verifyPushSubscription(user?.id, currentSettings);
|
||||
setWebPushEnabled(enabled);
|
||||
};
|
||||
|
||||
if (user?.id) {
|
||||
verifyWebPush();
|
||||
}
|
||||
}, [user?.id, currentSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSubscriptionEndpoint = async () => {
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
const { subscription } = await getPushSubscription();
|
||||
|
||||
if (subscription) {
|
||||
setSubEndpoint(subscription.endpoint);
|
||||
} else {
|
||||
setSubEndpoint(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getSubscriptionEndpoint();
|
||||
}, [webPushEnabled]);
|
||||
|
||||
const sortedDevices = useMemo(() => {
|
||||
if (!dataDevices || !subEndpoint) {
|
||||
return dataDevices;
|
||||
}
|
||||
|
||||
return [...dataDevices].sort((a, b) => {
|
||||
if (a.endpoint === subEndpoint) {
|
||||
return -1;
|
||||
}
|
||||
if (b.endpoint === subEndpoint) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
}, [dataDevices, subEndpoint]);
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
@@ -324,22 +311,18 @@ const UserWebPushSettings = () => {
|
||||
{intl.formatMessage(messages.managedevices)}
|
||||
</h3>
|
||||
<div className="section">
|
||||
{dataDevices?.length ? (
|
||||
dataDevices
|
||||
?.sort((a, b) => {
|
||||
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
})
|
||||
.map((device, index) => (
|
||||
<div className="py-2" key={`device-list-${index}`}>
|
||||
<DeviceItem
|
||||
key={index}
|
||||
disablePushNotifications={disablePushNotifications}
|
||||
device={device}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
{sortedDevices?.length ? (
|
||||
sortedDevices.map((device) => (
|
||||
<div className="py-2" key={`device-list-${device.endpoint}`}>
|
||||
<DeviceItem
|
||||
deletePushSubscriptionFromBackend={
|
||||
deletePushSubscriptionFromBackend
|
||||
}
|
||||
device={device}
|
||||
subEndpoint={subEndpoint}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<Alert
|
||||
|
||||
@@ -1436,6 +1436,7 @@
|
||||
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.noPermissionDescription": "You do not have permission to modify this user's linked accounts.",
|
||||
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorExists": "This account is already linked to a Plex user",
|
||||
"components.UserProfile.UserSettings.UserLinkedAccountsSettings.plexErrorUnauthorized": "Unable to connect to Plex using your credentials",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.activesubscription": "Active Subscription",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.browser": "Browser",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.created": "Created",
|
||||
"components.UserProfile.UserSettings.UserNotificationSettings.UserNotificationsWebPush.deletesubscription": "Delete Subscription",
|
||||
|
||||
162
src/utils/pushSubscriptionHelpers.ts
Normal file
162
src/utils/pushSubscriptionHelpers.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import type { UserPushSubscription } from '@server/entity/UserPushSubscription';
|
||||
import type { PublicSettingsResponse } from '@server/interfaces/api/settingsInterfaces';
|
||||
import axios from 'axios';
|
||||
|
||||
// Taken from https://www.npmjs.com/package/web-push
|
||||
function urlBase64ToUint8Array(base64String: string) {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = `${base64String}${padding}`
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i)
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
export const getPushSubscription = async () => {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
return { registration, subscription };
|
||||
};
|
||||
|
||||
export const verifyPushSubscription = async (
|
||||
userId: number | undefined,
|
||||
currentSettings: PublicSettingsResponse
|
||||
): Promise<boolean> => {
|
||||
if (!('serviceWorker' in navigator) || !userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { subscription } = await getPushSubscription();
|
||||
|
||||
if (!subscription) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const appServerKey = subscription.options?.applicationServerKey;
|
||||
if (!(appServerKey instanceof ArrayBuffer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentServerKey = new Uint8Array(appServerKey).toString();
|
||||
const expectedServerKey = urlBase64ToUint8Array(
|
||||
currentSettings.vapidPublic
|
||||
).toString();
|
||||
|
||||
const endpoint = subscription.endpoint;
|
||||
|
||||
const { data } = await axios.get<UserPushSubscription>(
|
||||
`/api/v1/user/${userId}/pushSubscription/${encodeURIComponent(endpoint)}`
|
||||
);
|
||||
|
||||
return expectedServerKey === currentServerKey && data.endpoint === endpoint;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyAndResubscribePushSubscription = async (
|
||||
userId: number | undefined,
|
||||
currentSettings: PublicSettingsResponse
|
||||
): Promise<boolean> => {
|
||||
const isValid = await verifyPushSubscription(userId, currentSettings);
|
||||
|
||||
if (isValid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentSettings.enablePushRegistration) {
|
||||
try {
|
||||
// Unsubscribe from the backend to clear the existing push subscription (keys and endpoint)
|
||||
await unsubscribeToPushNotifications(userId);
|
||||
|
||||
// Subscribe again to generate a fresh push subscription with updated keys and endpoint
|
||||
await subscribeToPushNotifications(userId, currentSettings);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new Error(`[SW] Resubscribe failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const subscribeToPushNotifications = async (
|
||||
userId: number | undefined,
|
||||
currentSettings: PublicSettingsResponse
|
||||
) => {
|
||||
if (
|
||||
!('serviceWorker' in navigator) ||
|
||||
!userId ||
|
||||
!currentSettings.enablePushRegistration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { registration } = await getPushSubscription();
|
||||
|
||||
if (!registration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: currentSettings.vapidPublic,
|
||||
});
|
||||
|
||||
const { endpoint, keys } = subscription.toJSON();
|
||||
|
||||
if (keys?.p256dh && keys?.auth) {
|
||||
await axios.post('/api/v1/user/registerPushSubscription', {
|
||||
endpoint,
|
||||
p256dh: keys.p256dh,
|
||||
auth: keys.auth,
|
||||
userAgent: navigator.userAgent,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Issue subscribing to push notifications: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const unsubscribeToPushNotifications = async (
|
||||
userId: number | undefined,
|
||||
endpoint?: string
|
||||
) => {
|
||||
if (!('serviceWorker' in navigator) || !userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { subscription } = await getPushSubscription();
|
||||
|
||||
if (!subscription) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { endpoint: currentEndpoint } = subscription.toJSON();
|
||||
|
||||
if (!endpoint || endpoint === currentEndpoint) {
|
||||
await subscription.unsubscribe();
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Issue unsubscribing to push notifications: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user