-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
5d8ed13
commit 779eb19
Showing
17 changed files
with
465 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
self.addEventListener("push", (event) => { | ||
let notificationData = event.data.json(); | ||
|
||
const title = notificationData.title || "You have new notification from Cal.com"; | ||
const image ="/cal-com-icon.svg"; | ||
const options = { | ||
...notificationData.options, | ||
icon: image, | ||
}; | ||
self.registration.showNotification(title, options); | ||
}); | ||
|
||
self.addEventListener("notificationclick", (event) => { | ||
event.notification.close(); | ||
event.waitUntil(self.clients.openWindow(event.notification.data.targetURL || "https://app.cal.com")); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import webpush from "web-push"; | ||
|
||
const vapidKeys = { | ||
publicKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || "", | ||
privateKey: process.env.VAPID_PRIVATE_KEY || "", | ||
}; | ||
|
||
// The mail to email address should be the one at which push service providers can reach you. It can also be a URL. | ||
webpush.setVapidDetails("https://cal.com", vapidKeys.publicKey, vapidKeys.privateKey); | ||
|
||
type Subscription = { | ||
endpoint: string; | ||
keys: { | ||
auth: string; | ||
p256dh: string; | ||
}; | ||
}; | ||
|
||
export const sendNotification = async ({ | ||
subscription, | ||
title, | ||
body, | ||
icon, | ||
}: { | ||
subscription: Subscription; | ||
title: string; | ||
body: string; | ||
icon?: string; | ||
}) => { | ||
try { | ||
const payload = JSON.stringify({ | ||
title, | ||
body, | ||
icon, | ||
}); | ||
await webpush.sendNotification(subscription, payload); | ||
} catch (error) { | ||
console.error("Error sending notification", error); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
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; | ||
}; |
14 changes: 14 additions & 0 deletions
14
...es/prisma/migrations/20240506065443_added_notifications_subscriptions_table/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
-- CreateTable | ||
CREATE TABLE "NotificationsSubscriptions" ( | ||
"id" SERIAL NOT NULL, | ||
"userId" INTEGER NOT NULL, | ||
"subscription" TEXT NOT NULL, | ||
|
||
CONSTRAINT "NotificationsSubscriptions_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "NotificationsSubscriptions_userId_subscription_idx" ON "NotificationsSubscriptions"("userId", "subscription"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "NotificationsSubscriptions" ADD CONSTRAINT "NotificationsSubscriptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.