Files
calendar/packages/lib/hooks/useNotifications.tsx
T
779eb19e7a feat: browser push notifications (#14888)
* feat: web push notifications feature

* Revert yarn.lock changes

* added new env variables requirement in .env.example

* moved useNotifications hook in packages/lib/hooks

* fix: bug

* use i18n

* chore: move to new file

* chore: add yarn.lock

* Update .env.example

Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>

* chore: add instruction for brave browser

* fix: tooltip

* chore: use enum

* chore

* small update

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-09-02 14:39:51 +00:00

165 lines
4.7 KiB
TypeScript

import { useState, useEffect } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { showToast } from "@calcom/ui";
export enum ButtonState {
NONE = "none",
ALLOW = "allow",
DISABLE = "disable",
DENIED = "denied",
}
export const useNotifications = () => {
const [buttonToShow, setButtonToShow] = useState<ButtonState>(ButtonState.NONE);
const [isLoading, setIsLoading] = useState(false);
const { t } = useLocale();
const { mutate: addSubscription } = trpc.viewer.addNotificationsSubscription.useMutation({
onSuccess: () => {
setButtonToShow(ButtonState.DISABLE);
showToast(t("browser_notifications_turned_on"), "success");
},
onError: (error) => {
showToast(`Error: ${error.message}`, "error");
},
onSettled: () => {
setIsLoading(false);
},
});
const { mutate: removeSubscription } = trpc.viewer.removeNotificationsSubscription.useMutation({
onSuccess: () => {
setButtonToShow(ButtonState.ALLOW);
showToast(t("browser_notifications_turned_off"), "success");
},
onError: (error) => {
showToast(`Error: ${error.message}`, "error");
},
onSettled: () => {
setIsLoading(false);
},
});
useEffect(() => {
const decideButtonToShow = async () => {
if (!("Notification" in window)) {
console.log("Notifications not supported");
}
const registration = await navigator.serviceWorker?.getRegistration();
if (!registration) return;
const subscription = await registration.pushManager.getSubscription();
const permission = Notification.permission;
if (permission === ButtonState.DENIED) {
setButtonToShow(ButtonState.DENIED);
return;
}
if (permission === "default") {
setButtonToShow(ButtonState.ALLOW);
return;
}
if (!subscription) {
setButtonToShow(ButtonState.ALLOW);
return;
}
setButtonToShow(ButtonState.DISABLE);
};
decideButtonToShow();
}, []);
const enableNotifications = async () => {
setIsLoading(true);
const permissionResponse = await Notification.requestPermission();
if (permissionResponse === ButtonState.DENIED) {
setButtonToShow(ButtonState.DENIED);
setIsLoading(false);
showToast(t("browser_notifications_denied"), "warning");
return;
}
if (permissionResponse === "default") {
setIsLoading(false);
showToast(t("please_allow_notifications"), "warning");
return;
}
const registration = await navigator.serviceWorker?.getRegistration();
if (!registration) {
// This will not happen ideally as the button will not be shown if the service worker is not registered
return;
}
let subscription: PushSubscription;
try {
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlB64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || ""),
});
} catch (error) {
// This happens in Brave browser as it does not have a push service
console.error(error);
setIsLoading(false);
setButtonToShow(ButtonState.NONE);
showToast(t("browser_notifications_not_supported"), "error");
return;
}
addSubscription(
{ subscription: JSON.stringify(subscription) },
{
onError: async () => {
await subscription.unsubscribe();
},
}
);
};
const disableNotifications = async () => {
setIsLoading(true);
const registration = await navigator.serviceWorker?.getRegistration();
if (!registration) {
// This will not happen ideally as the button will not be shown if the service worker is not registered
return;
}
const subscription = await registration.pushManager.getSubscription();
if (!subscription) {
// This will not happen ideally as the button will not be shown if the subscription is not present
return;
}
removeSubscription(
{ subscription: JSON.stringify(subscription) },
{
onSuccess: async () => {
await subscription.unsubscribe();
},
}
);
};
return {
buttonToShow,
isLoading,
enableNotifications,
disableNotifications,
};
};
const urlB64ToUint8Array = (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;
};