-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: notifications feature for unconfirmed events
- Loading branch information
1 parent
5c2b561
commit d085c1d
Showing
16 changed files
with
572 additions
and
5,497 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { useState, useEffect } from "react"; | ||
|
||
import { trpc } from "@calcom/trpc/react"; | ||
import { showToast } from "@calcom/ui"; | ||
|
||
export const useNotifications = () => { | ||
const [buttonToShow, setButtonToShow] = useState<"none" | "allow" | "disable" | "denied">("none"); | ||
const [isLoading, setIsLoading] = useState(false); | ||
|
||
const { mutate: addSubscription } = trpc.viewer.addNotificationsSubscription.useMutation({ | ||
onSuccess: () => { | ||
setButtonToShow("disable"); | ||
showToast("Notifications turned on", "success"); | ||
}, | ||
onError: (error) => { | ||
showToast(`Error: ${error.message}`, "error"); | ||
}, | ||
onSettled: () => { | ||
setIsLoading(false); | ||
}, | ||
}); | ||
const { mutate: removeSubscription } = trpc.viewer.removeNotificationsSubscription.useMutation({ | ||
onSuccess: () => { | ||
setButtonToShow("allow"); | ||
showToast("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 === "denied") { | ||
setButtonToShow("denied"); | ||
return; | ||
} | ||
|
||
if (permission === "default") { | ||
setButtonToShow("allow"); | ||
return; | ||
} | ||
|
||
if (!subscription) { | ||
setButtonToShow("allow"); | ||
return; | ||
} | ||
|
||
setButtonToShow("disable"); | ||
}; | ||
|
||
decideButtonToShow(); | ||
}, []); | ||
|
||
const enableNotifications = async () => { | ||
setIsLoading(true); | ||
const permissionResponse = await Notification.requestPermission(); | ||
|
||
if (permissionResponse === "denied") { | ||
setButtonToShow("denied"); | ||
setIsLoading(false); | ||
showToast("You denied the notifications", "warning"); | ||
return; | ||
} | ||
|
||
if (permissionResponse === "default") { | ||
setIsLoading(false); | ||
showToast("Please allow notifications from the prompt", "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; | ||
} | ||
|
||
const subscription = await registration.pushManager.subscribe({ | ||
userVisibleOnly: true, | ||
applicationServerKey: urlB64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || ""), | ||
}); | ||
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; | ||
}; |
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,25 @@ | ||
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("mailto:web-push-book@gauntface.com", vapidKeys.publicKey, vapidKeys.privateKey); | ||
|
||
type Subscription = { | ||
endpoint: string; | ||
keys: { | ||
auth: string; | ||
p256dh: string; | ||
}; | ||
}; | ||
|
||
export const sendNotification = async (subscription: Subscription, payload: string) => { | ||
try { | ||
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
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.