Skip to content

Commit 9817277

Browse files
committed
feat: create notification page
1 parent 44d4185 commit 9817277

File tree

9 files changed

+184
-208
lines changed

9 files changed

+184
-208
lines changed

apps/web/app/providers.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import { TrpcProvider } from "app/_trpc/trpc-provider";
44
import { SessionProvider } from "next-auth/react";
55
import CacheProvider from "react-inlinesvg/provider";
66

7+
import { WebPushProvider } from "@calcom/features/notifications/WebPushContext";
8+
79
export function Providers({ children }: { children: React.ReactNode }) {
810
return (
911
<SessionProvider>
1012
<TrpcProvider>
1113
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
12-
<CacheProvider>{children}</CacheProvider>
14+
<CacheProvider>
15+
<WebPushProvider>{children}</WebPushProvider>
16+
</CacheProvider>
1317
</TrpcProvider>
1418
</SessionProvider>
1519
);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { getTranslate } from "app/_utils";
2+
import { _generateMetadata } from "app/_utils";
3+
4+
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
5+
6+
import PushNotificationsView from "~/settings/my-account/push-notifications-view";
7+
8+
export const generateMetadata = async () =>
9+
await _generateMetadata(
10+
(t) => t("push_notifications"),
11+
(t) => t("push_notifications_description")
12+
);
13+
14+
const Page = async () => {
15+
const t = await getTranslate();
16+
17+
return (
18+
<SettingsHeader
19+
title={t("push_notifications")}
20+
description={t("push_notifications_description")}
21+
borderInShellHeader={true}>
22+
<PushNotificationsView />
23+
</SettingsHeader>
24+
);
25+
};
26+
27+
export default Page;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use client";
2+
3+
import { useWebPush } from "@calcom/features/notifications/WebPushContext";
4+
import { useLocale } from "@calcom/lib/hooks/useLocale";
5+
import { Button } from "@calcom/ui";
6+
7+
const PushNotificationsView = () => {
8+
const { t } = useLocale();
9+
const { subscribe, unsubscribe, isSubscribed, isLoading } = useWebPush();
10+
11+
return (
12+
<div className="border-subtle rounded-b-xl border-x border-b px-4 pb-10 pt-8 sm:px-6">
13+
<Button color="primary" onClick={isSubscribed ? unsubscribe : subscribe} disabled={isLoading}>
14+
{isSubscribed ? t("disable_browser_notifications") : t("allow_browser_notifications")}
15+
</Button>
16+
</div>
17+
);
18+
};
19+
20+
export default PushNotificationsView;

apps/web/pages/_app.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { IncomingMessage } from "http";
22
import { SessionProvider } from "next-auth/react";
33
import type { AppContextType } from "next/dist/shared/lib/utils";
4-
import React, { useEffect } from "react";
4+
import React from "react";
55
import CacheProvider from "react-inlinesvg/provider";
66

7+
import { WebPushProvider } from "@calcom/features/notifications/WebPushContext";
78
import { trpc } from "@calcom/trpc/react";
89

910
import type { AppProps } from "@lib/app-providers";
@@ -13,18 +14,14 @@ import "../styles/globals.css";
1314
function MyApp(props: AppProps) {
1415
const { Component, pageProps } = props;
1516

16-
useEffect(() => {
17-
if (typeof window !== "undefined" && "serviceWorker" in navigator) {
18-
navigator.serviceWorker.register("/service-worker.js");
19-
}
20-
}, []);
21-
2217
return (
2318
<SessionProvider session={pageProps.session ?? undefined}>
24-
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
25-
<CacheProvider>
26-
{Component.PageWrapper ? <Component.PageWrapper {...props} /> : <Component {...pageProps} />}
27-
</CacheProvider>
19+
<WebPushProvider>
20+
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
21+
<CacheProvider>
22+
{Component.PageWrapper ? <Component.PageWrapper {...props} /> : <Component {...pageProps} />}
23+
</CacheProvider>
24+
</WebPushProvider>
2825
</SessionProvider>
2926
);
3027
}

apps/web/public/static/locales/en/common.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,8 @@
489489
"browser_notifications_turned_off": "Browser Notifications turned off",
490490
"browser_notifications_denied": "Browser Notifications denied",
491491
"please_allow_notifications": "Please allow notifications from the prompt",
492+
"push_notifications": "Push Notifications",
493+
"push_notifications_description": "Receive push notifications when booker submits instant meeting booking.",
492494
"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",
493495
"email": "Email",
494496
"email_placeholder": "[email protected]",
@@ -2921,4 +2923,4 @@
29212923
"enter_option_value": "Enter option value",
29222924
"remove_option": "Remove option",
29232925
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
2924-
}
2926+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"use client";
2+
3+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
4+
5+
import { trpc } from "@calcom/trpc/react";
6+
import { showToast } from "@calcom/ui";
7+
8+
interface WebPushContextProps {
9+
permission: NotificationPermission;
10+
isLoading: boolean;
11+
isSubscribed: boolean;
12+
subscribe: () => Promise<void>;
13+
unsubscribe: () => Promise<void>;
14+
}
15+
16+
const WebPushContext = createContext<WebPushContextProps | null>(null);
17+
18+
interface ProviderProps {
19+
children: React.ReactNode;
20+
}
21+
22+
export function WebPushProvider({ children }: ProviderProps) {
23+
const [permission, setPermission] = useState<NotificationPermission>(() =>
24+
typeof window !== "undefined" && "Notification" in window ? Notification.permission : "denied"
25+
);
26+
const [pushManager, setPushManager] = useState<PushManager | null>(null);
27+
const [isLoading, setIsLoading] = useState(false);
28+
const [isSubscribed, setIsSubscribed] = useState(false);
29+
30+
const { mutate: addSubscription } = trpc.viewer.addNotificationsSubscription.useMutation();
31+
const { mutate: removeSubscription } = trpc.viewer.removeNotificationsSubscription.useMutation();
32+
33+
useEffect(() => {
34+
if (!("serviceWorker" in navigator)) return;
35+
36+
navigator.serviceWorker
37+
.register("/service-worker.js")
38+
.then(async (registration) => {
39+
if ("pushManager" in registration) {
40+
setPushManager(registration.pushManager);
41+
const subscription = await registration.pushManager.getSubscription();
42+
setIsSubscribed(!!subscription);
43+
}
44+
})
45+
.catch((error) => {
46+
console.error("Service Worker registration failed:", error);
47+
});
48+
}, []);
49+
50+
const contextValue = useMemo(
51+
() => ({
52+
permission,
53+
isLoading,
54+
isSubscribed,
55+
subscribe: async () => {
56+
try {
57+
setIsLoading(true);
58+
const newPermission = await Notification.requestPermission();
59+
setPermission(newPermission);
60+
61+
if (newPermission === "granted" && pushManager) {
62+
const subscription = await pushManager.subscribe({
63+
userVisibleOnly: true,
64+
applicationServerKey: urlB64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || ""),
65+
});
66+
addSubscription({ subscription: JSON.stringify(subscription) });
67+
setIsSubscribed(true);
68+
showToast("Notifications enabled successfully", "success");
69+
}
70+
} catch (error) {
71+
console.error("Failed to subscribe:", error);
72+
showToast("Failed to enable notifications", "error");
73+
} finally {
74+
setIsLoading(false);
75+
}
76+
},
77+
unsubscribe: async () => {
78+
if (!pushManager) return;
79+
try {
80+
setIsLoading(true);
81+
const subscription = await pushManager.getSubscription();
82+
if (subscription) {
83+
const subscriptionJson = JSON.stringify(subscription);
84+
await subscription.unsubscribe();
85+
removeSubscription({ subscription: subscriptionJson });
86+
setIsSubscribed(false);
87+
showToast("Notifications disabled successfully", "success");
88+
}
89+
} catch (error) {
90+
console.error("Failed to unsubscribe:", error);
91+
showToast("Failed to disable notifications", "error");
92+
} finally {
93+
setIsLoading(false);
94+
}
95+
},
96+
}),
97+
[permission, isLoading, isSubscribed, pushManager, addSubscription, removeSubscription]
98+
);
99+
100+
return <WebPushContext.Provider value={contextValue}>{children}</WebPushContext.Provider>;
101+
}
102+
103+
export function useWebPush() {
104+
const context = useContext(WebPushContext);
105+
if (!context) {
106+
throw new Error("useWebPush must be used within a WebPushProvider");
107+
}
108+
return context;
109+
}
110+
111+
const urlB64ToUint8Array = (base64String: string) => {
112+
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
113+
const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/");
114+
const rawData = window.atob(base64);
115+
const outputArray = new Uint8Array(rawData.length);
116+
for (let i = 0; i < rawData.length; ++i) {
117+
outputArray[i] = rawData.charCodeAt(i);
118+
}
119+
return outputArray;
120+
};

packages/features/settings/appDir/SettingsLayoutAppDirClient.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
3535
{ name: "conferencing", href: "/settings/my-account/conferencing" },
3636
{ name: "appearance", href: "/settings/my-account/appearance" },
3737
{ name: "out_of_office", href: "/settings/my-account/out-of-office" },
38+
{ name: "push_notifications", href: "/settings/my-account/push-notifications" },
3839
// TODO
3940
// { name: "referrals", href: "/settings/my-account/referrals" },
4041
],

packages/features/shell/Shell.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import classNames from "@calcom/lib/classNames";
1212
import { APP_NAME } from "@calcom/lib/constants";
1313
import { useFormbricks } from "@calcom/lib/formbricks-client";
1414
import { useLocale } from "@calcom/lib/hooks/useLocale";
15-
import { ButtonState, useNotifications } from "@calcom/lib/hooks/useNotifications";
1615
import { Button, ErrorBoundary, HeadSeo, SkeletonText } from "@calcom/ui";
1716

1817
import { SideBarContainer } from "./SideBar";
@@ -136,8 +135,6 @@ export function ShellMain(props: LayoutProps) {
136135
const router = useRouter();
137136
const { isLocaleReady, t } = useLocale();
138137

139-
const { buttonToShow, isLoading, enableNotifications, disableNotifications } = useNotifications();
140-
141138
return (
142139
<>
143140
{(props.heading || !!props.backPath) && (
@@ -196,23 +193,6 @@ export function ShellMain(props: LayoutProps) {
196193
</div>
197194
)}
198195
{props.actions && props.actions}
199-
{props.heading === "Bookings" && buttonToShow && (
200-
<Button
201-
color="primary"
202-
onClick={buttonToShow === ButtonState.ALLOW ? enableNotifications : disableNotifications}
203-
loading={isLoading}
204-
disabled={buttonToShow === ButtonState.DENIED}
205-
tooltipSide="bottom"
206-
tooltip={
207-
buttonToShow === ButtonState.DENIED ? t("you_have_denied_notifications") : undefined
208-
}>
209-
{t(
210-
buttonToShow === ButtonState.DISABLE
211-
? "disable_browser_notifications"
212-
: "allow_browser_notifications"
213-
)}
214-
</Button>
215-
)}
216196
</header>
217197
)}
218198
</div>

0 commit comments

Comments
 (0)