Files
calendar/packages/lib/hooks/useNotifications.tsx
T
Udit TakkarandGitHub 860d173f0a feat: instant meeting browser notifcations (#16480)
* feat: instant meeting browser notifcations

* chore: support multiple notifications

* chore: save progress

* fix: add test notification and move file

* fix: type err

* chore: feedback improvements

* chore: undo DENY
2024-09-11 18:02:14 +04:00

166 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;
};