diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx index aacc629b615e79..6986fcfc0d46f6 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx @@ -35,6 +35,7 @@ const getTabs = (orgBranding: OrganizationBranding | null) => { { name: "conferencing", href: "/settings/my-account/conferencing" }, { name: "appearance", href: "/settings/my-account/appearance" }, { name: "out_of_office", href: "/settings/my-account/out-of-office" }, + { name: "push_notifications", href: "/settings/my-account/push-notifications" }, // TODO // { name: "referrals", href: "/settings/my-account/referrals" }, ], diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 3e5545b6c0d5f6..75fa7f388924a7 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -6,6 +6,7 @@ import React from "react"; import { getLocale } from "@calcom/features/auth/lib/getLocale"; import { IconSprites } from "@calcom/ui"; +import { NotificationSoundHandler } from "@calcom/web/components/notification-sound-handler"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; import { prepareRootMetadata } from "@lib/metadata"; @@ -124,6 +125,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo ]} /> {children} + {!isEmbed && } + ); diff --git a/apps/web/app/providers.tsx b/apps/web/app/providers.tsx index e218da68348000..2b3a5caad2b141 100644 --- a/apps/web/app/providers.tsx +++ b/apps/web/app/providers.tsx @@ -5,6 +5,8 @@ import { TrpcProvider } from "app/_trpc/trpc-provider"; import { SessionProvider } from "next-auth/react"; import CacheProvider from "react-inlinesvg/provider"; +import { WebPushProvider } from "@calcom/features/notifications/WebPushContext"; + import useIsBookingPage from "@lib/hooks/useIsBookingPage"; import PlainChat from "@lib/plain/dynamicProvider"; @@ -20,7 +22,9 @@ export function Providers({ children, dehydratedState }: ProvidersProps) { {!isBookingPage ? : null} {/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */} - {children} + + {children} + ); diff --git a/apps/web/app/settings/(settings-layout)/my-account/push-notifications/page.tsx b/apps/web/app/settings/(settings-layout)/my-account/push-notifications/page.tsx new file mode 100644 index 00000000000000..7242bd83e31741 --- /dev/null +++ b/apps/web/app/settings/(settings-layout)/my-account/push-notifications/page.tsx @@ -0,0 +1,27 @@ +import { getTranslate } from "app/_utils"; +import { _generateMetadata } from "app/_utils"; + +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import PushNotificationsView from "~/settings/my-account/push-notifications-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("push_notifications"), + (t) => t("push_notifications_description") + ); + +const Page = async () => { + const t = await getTranslate(); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/components/notification-sound-handler.tsx b/apps/web/components/notification-sound-handler.tsx new file mode 100644 index 00000000000000..8a57daa552c46b --- /dev/null +++ b/apps/web/components/notification-sound-handler.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +export function NotificationSoundHandler() { + const audioContextRef = useRef(null); + const sourceRef = useRef(null); + const audioBufferRef = useRef(null); + + const initializeAudioSystem = async () => { + try { + // Only create a new context if we don't have one or if it's closed + if (!audioContextRef.current || audioContextRef.current.state === "closed") { + audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + + if (audioContextRef.current.state === "suspended") { + await audioContextRef.current.resume(); + } + + if (!audioBufferRef.current) { + const response = await fetch("/ring.mp3"); + const arrayBuffer = await response.arrayBuffer(); + audioBufferRef.current = await audioContextRef.current.decodeAudioData(arrayBuffer); + console.log("Audio file loaded and decoded"); + } + + return true; + } catch (error) { + console.error("Failed to initialize audio system:", error); + return false; + } + }; + + const playSound = async () => { + try { + // Ensure audio system is initialized + if (!audioContextRef.current || audioContextRef.current.state === "closed") { + const initialized = await initializeAudioSystem(); + if (!initialized) return; + } + + if (!audioContextRef.current) return; + + // Resume context if suspended + if (audioContextRef.current.state === "suspended") { + await audioContextRef.current.resume(); + } + + // Stop any currently playing sound + if (sourceRef.current) { + sourceRef.current.stop(); + sourceRef.current.disconnect(); + sourceRef.current = null; + } + + // Create and play new sound + const source = audioContextRef.current.createBufferSource(); + source.buffer = audioBufferRef.current; + source.connect(audioContextRef.current.destination); + sourceRef.current = source; + + source.loop = true; + source.start(0); + console.log("Sound started playing"); + + setTimeout(() => { + if (sourceRef.current === source) { + source.stop(); + source.disconnect(); + sourceRef.current = null; + } + }, 7000); + } catch (error) { + console.error("Error playing sound:", error); + // Try to reinitialize on error + audioContextRef.current = null; + audioBufferRef.current = null; + } + }; + + const stopSound = () => { + if (sourceRef.current) { + sourceRef.current.stop(); + sourceRef.current.disconnect(); + sourceRef.current = null; + } + }; + + useEffect(() => { + // Don't automatically initialize - wait for user interaction + return () => { + stopSound(); + }; + }, []); + + useEffect(() => { + if (!("serviceWorker" in navigator)) { + console.log("ServiceWorker not available"); + return; + } + + const messageHandler = async (event: MessageEvent) => { + if (event.data.type === "PLAY_NOTIFICATION_SOUND") { + // Only initialize if not already initialized + if (!audioBufferRef.current) { + await initializeAudioSystem(); + } + await playSound(); + } + + if (event.data.type === "STOP_NOTIFICATION_SOUND") { + stopSound(); + } + }; + + navigator.serviceWorker.addEventListener("message", messageHandler); + return () => navigator.serviceWorker.removeEventListener("message", messageHandler); + }, []); + + // Single click handler for initial audio setup + useEffect(() => { + const handleFirstInteraction = async () => { + // Only initialize if not already initialized + if (!audioBufferRef.current) { + await initializeAudioSystem(); + } + document.removeEventListener("click", handleFirstInteraction); + document.removeEventListener("touchstart", handleFirstInteraction); + }; + + document.addEventListener("click", handleFirstInteraction); + document.addEventListener("touchstart", handleFirstInteraction); + + return () => { + document.removeEventListener("click", handleFirstInteraction); + document.removeEventListener("touchstart", handleFirstInteraction); + }; + }, []); + + return null; +} diff --git a/apps/web/modules/settings/my-account/push-notifications-view.tsx b/apps/web/modules/settings/my-account/push-notifications-view.tsx new file mode 100644 index 00000000000000..bdd7a192327831 --- /dev/null +++ b/apps/web/modules/settings/my-account/push-notifications-view.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useWebPush } from "@calcom/features/notifications/WebPushContext"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button } from "@calcom/ui"; + +const PushNotificationsView = () => { + const { t } = useLocale(); + const { subscribe, unsubscribe, isSubscribed, isLoading } = useWebPush(); + + return ( +
+ +
+ ); +}; + +export default PushNotificationsView; diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index fdc0f8c01d2faf..5ae26bac6a287f 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,9 +1,10 @@ import type { IncomingMessage } from "http"; import { SessionProvider } from "next-auth/react"; import type { AppContextType } from "next/dist/shared/lib/utils"; -import React, { useEffect } from "react"; +import React from "react"; import CacheProvider from "react-inlinesvg/provider"; +import { WebPushProvider } from "@calcom/features/notifications/WebPushContext"; import { trpc } from "@calcom/trpc/react"; import type { AppProps } from "@lib/app-providers"; @@ -13,18 +14,14 @@ import "../styles/globals.css"; function MyApp(props: AppProps) { const { Component, pageProps } = props; - useEffect(() => { - if (typeof window !== "undefined" && "serviceWorker" in navigator) { - navigator.serviceWorker.register("/service-worker.js"); - } - }, []); - return ( - {/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */} - - {Component.PageWrapper ? : } - + + {/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */} + + {Component.PageWrapper ? : } + + ); } diff --git a/apps/web/public/ring.mp3 b/apps/web/public/ring.mp3 new file mode 100644 index 00000000000000..d6de8ec748b8b1 Binary files /dev/null and b/apps/web/public/ring.mp3 differ diff --git a/apps/web/public/service-worker.js b/apps/web/public/service-worker.js index 47cc9a72e60625..4d3759d85814e1 100644 --- a/apps/web/public/service-worker.js +++ b/apps/web/public/service-worker.js @@ -1,4 +1,6 @@ self.addEventListener("push", async (event) => { + if (!event.data) return + let notificationData = event.data.json(); const allClients = await clients.matchAll({ @@ -11,43 +13,82 @@ self.addEventListener("push", async (event) => { return; } - const title = notificationData.title || "You have a new notification from Cal.com"; - const image = "https://cal.com/api/logo?type=icon"; - const newNotificationOptions = { - requireInteraction: true, - ...notificationData, - icon: image, - badge: image, - data: { - url: notificationData.data?.url || "https://app.cal.com", - }, - silent: false, - vibrate: [300, 100, 400], - tag: `notification-${Date.now()}-${Math.random()}`, - }; - - const existingNotifications = await self.registration.getNotifications(); - - // Display each existing notification again to make sure old ones can still be clicked - existingNotifications.forEach((notification) => { - const options = { - body: notification.body, - icon: notification.icon, - badge: notification.badge, - data: notification.data, - silent: notification.silent, - vibrate: notification.vibrate, - requireInteraction: notification.requireInteraction, - tag: notification.tag, - }; - self.registration.showNotification(notification.title, options); - }); + const title = notificationData.title || "New Cal.com Notification"; + const image = notificationData.icon || "https://cal.com/api/logo?type=icon"; + + event.waitUntil( + (async () => { + try { + // Close notifications with the same tag if it exists + const existingNotifications = await self.registration.getNotifications({ + tag: notificationData.tag + }); + + existingNotifications.forEach((notification) => { + const options = { + body: notification.body, + icon: notification.icon, + badge: notification.badge, + data: notification.data, + silent: notification.silent, + vibrate: notification.vibrate, + requireInteraction: notification.requireInteraction, + tag: notification.tag, + }; + + self.registration.showNotification(notification.title, options); + }); + + // Special handling for instant meetings + if (notificationData.data?.type === "INSTANT_MEETING") { + allClients.forEach(client => { + client.postMessage({ + type: 'PLAY_NOTIFICATION_SOUND' + }); + }); + } + + const notificationOptions = { + body: notificationData.body, + icon: image, + badge: image, + data: notificationData.data, + tag: notificationData.tag || `cal-notification-${Date.now()}`, + renotify: true, + requireInteraction: notificationData.requireInteraction ?? true, + actions: notificationData.actions || [], + vibrate: [200, 100, 200], + urgency: 'high' + }; + + console.log("notificationOptions", notificationOptions); - // Show the new notification - self.registration.showNotification(title, newNotificationOptions); + await self.registration.showNotification(title, notificationOptions); + console.log("Notification shown successfully"); + } catch (error) { + console.error("Error showing notification:", error); + } + })() + ); }); + self.addEventListener("notificationclick", (event) => { + if (event.notification.data?.type === "INSTANT_MEETING") { + const stopSound = async () => { + const allClients = await clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + + allClients.forEach(client => { + client.postMessage({ + type: 'STOP_NOTIFICATION_SOUND' + }); + }); + }; + } + if (!event.action) { // Normal Notification Click event.notification.close(); @@ -63,3 +104,11 @@ self.addEventListener("notificationclick", (event) => { break; } }); + +self.addEventListener('install', (event) => { + console.log('Service Worker installing.'); +}); + +self.addEventListener('activate', (event) => { + console.log('Service Worker activated.'); +}); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index cdcb3001b3e92e..dbedce04f705ee 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -498,6 +498,8 @@ "browser_notifications_turned_off": "Browser Notifications turned off", "browser_notifications_denied": "Browser Notifications denied", "please_allow_notifications": "Please allow notifications from the prompt", + "push_notifications": "Push Notifications", + "push_notifications_description": "Receive push notifications when booker submits instant meeting booking.", "browser_notifications_not_supported": "Your browser does not support Push Notifications. If you are Brave user then enable `Use Google services for push messaging` Option on brave://settings/?search=push+messaging", "email": "Email", "email_placeholder": "jdoe@example.com", diff --git a/packages/features/instant-meeting/handleInstantMeeting.ts b/packages/features/instant-meeting/handleInstantMeeting.ts index e10f25d49c953e..703ea79ed508c5 100644 --- a/packages/features/instant-meeting/handleInstantMeeting.ts +++ b/packages/features/instant-meeting/handleInstantMeeting.ts @@ -143,14 +143,8 @@ const triggerBrowserNotifications = async (args: { title: title, body: "User is waiting for you to join. Click to Connect", url: connectAndJoinUrl, - actions: [ - { - action: "connect-action", - title: "Connect and join", - type: "button", - image: "https://cal.com/api/logo?type=icon", - }, - ], + type: "INSTANT_MEETING", + requireInteraction: false, }); }); diff --git a/packages/features/notifications/WebPushContext.tsx b/packages/features/notifications/WebPushContext.tsx new file mode 100644 index 00000000000000..ffd93fd60e084a --- /dev/null +++ b/packages/features/notifications/WebPushContext.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { createContext, useContext, useEffect, useMemo, useState } from "react"; + +import { trpc } from "@calcom/trpc/react"; +import { showToast } from "@calcom/ui"; + +interface WebPushContextProps { + permission: NotificationPermission; + isLoading: boolean; + isSubscribed: boolean; + subscribe: () => Promise; + unsubscribe: () => Promise; +} + +const WebPushContext = createContext(null); + +interface ProviderProps { + children: React.ReactNode; +} + +export function WebPushProvider({ children }: ProviderProps) { + const [permission, setPermission] = useState(() => + typeof window !== "undefined" && "Notification" in window ? Notification.permission : "denied" + ); + const [pushManager, setPushManager] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isSubscribed, setIsSubscribed] = useState(false); + + const { mutate: addSubscription } = trpc.viewer.addNotificationsSubscription.useMutation(); + const { mutate: removeSubscription } = trpc.viewer.removeNotificationsSubscription.useMutation(); + + useEffect(() => { + if (!("serviceWorker" in navigator)) return; + + navigator.serviceWorker + .register("/service-worker.js") + .then(async (registration) => { + if ("pushManager" in registration) { + setPushManager(registration.pushManager); + const subscription = await registration.pushManager.getSubscription(); + setIsSubscribed(!!subscription); + } + }) + .catch((error) => { + console.error("Service Worker registration failed:", error); + }); + }, []); + + const contextValue = useMemo( + () => ({ + permission, + isLoading, + isSubscribed, + subscribe: async () => { + try { + setIsLoading(true); + const newPermission = await Notification.requestPermission(); + setPermission(newPermission); + + if (newPermission === "granted" && pushManager) { + const subscription = await pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlB64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || ""), + }); + addSubscription({ subscription: JSON.stringify(subscription) }); + setIsSubscribed(true); + showToast("Notifications enabled successfully", "success"); + } + } catch (error) { + console.error("Failed to subscribe:", error); + showToast("Failed to enable notifications", "error"); + } finally { + setIsLoading(false); + } + }, + unsubscribe: async () => { + if (!pushManager) return; + try { + setIsLoading(true); + const subscription = await pushManager.getSubscription(); + if (subscription) { + const subscriptionJson = JSON.stringify(subscription); + await subscription.unsubscribe(); + removeSubscription({ subscription: subscriptionJson }); + setIsSubscribed(false); + showToast("Notifications disabled successfully", "success"); + } + } catch (error) { + console.error("Failed to unsubscribe:", error); + showToast("Failed to disable notifications", "error"); + } finally { + setIsLoading(false); + } + }, + }), + [permission, isLoading, isSubscribed, pushManager, addSubscription, removeSubscription] + ); + + return {children}; +} + +export function useWebPush() { + const context = useContext(WebPushContext); + if (!context) { + throw new Error("useWebPush must be used within a WebPushProvider"); + } + return context; +} + +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; +}; diff --git a/packages/features/notifications/sendNotification.ts b/packages/features/notifications/sendNotification.ts index 3a603f09d5af0d..ab04353e1bb98a 100644 --- a/packages/features/notifications/sendNotification.ts +++ b/packages/features/notifications/sendNotification.ts @@ -6,7 +6,7 @@ const vapidKeys = { }; // 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); +webpush.setVapidDetails("mailto:support@cal.com", vapidKeys.publicKey, vapidKeys.privateKey); type Subscription = { endpoint: string; @@ -24,6 +24,7 @@ export const sendNotification = async ({ url, actions, requireInteraction, + type = "INSTANT_MEETING", }: { subscription: Subscription; title: string; @@ -32,6 +33,7 @@ export const sendNotification = async ({ url?: string; actions?: { action: string; title: string; type: string; image: string | null }[]; requireInteraction?: boolean; + type?: string; }) => { try { const payload = JSON.stringify({ @@ -40,9 +42,11 @@ export const sendNotification = async ({ icon, data: { url, + type, }, actions, requireInteraction, + tag: `cal-notification-${Date.now()}`, }); await webpush.sendNotification(subscription, payload); } catch (error) { diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 734af9f2e2cc31..7978655b983c2d 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -13,7 +13,6 @@ import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog import { APP_NAME } from "@calcom/lib/constants"; import { useFormbricks } from "@calcom/lib/formbricks-client"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { useNotifications } from "@calcom/lib/hooks/useNotifications"; import { Button, ErrorBoundary, HeadSeo, SkeletonText } from "@calcom/ui"; import classNames from "@calcom/ui/classNames"; @@ -137,8 +136,6 @@ export function ShellMain(props: LayoutProps) { const router = useRouter(); const { isLocaleReady, t } = useLocale(); - const { buttonToShow, isLoading, enableNotifications, disableNotifications } = useNotifications(); - return ( <> {(props.heading || !!props.backPath) && ( @@ -195,23 +192,6 @@ export function ShellMain(props: LayoutProps) { )} {props.actions && props.actions} - {/* TODO: temporary hide push notifications {props.heading === "Bookings" && buttonToShow && ( - - )} */} )} diff --git a/packages/lib/hooks/useNotifications.tsx b/packages/lib/hooks/useNotifications.tsx deleted file mode 100644 index bc1f18ce6dbd6b..00000000000000 --- a/packages/lib/hooks/useNotifications.tsx +++ /dev/null @@ -1,165 +0,0 @@ -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.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; -}; diff --git a/packages/trpc/server/routers/loggedInViewer/addNotificationsSubscription.handler.ts b/packages/trpc/server/routers/loggedInViewer/addNotificationsSubscription.handler.ts index 1a197a1eddb426..3f57f4776bdee6 100644 --- a/packages/trpc/server/routers/loggedInViewer/addNotificationsSubscription.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/addNotificationsSubscription.handler.ts @@ -59,6 +59,7 @@ export const addNotificationsSubscriptionHandler = async ({ ctx, input }: AddSec body: "Push Notifications activated successfully", url: "https://app.cal.com/", requireInteraction: false, + type: "TEST_NOTIFICATION", }); return {