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>
This commit is contained in:
co-authored by
Amit Sharma
Peer Richelsen
Udit Takkar
Udit Takkar
Peer Richelsen
Carina Wollendorfer
unknown
Anik Dhabal Babu
parent
5d8ed130de
commit
779eb19e7a
@@ -369,6 +369,11 @@ RETELL_AI_KEY=
|
||||
# Used to disallow emails as being added as guests on bookings
|
||||
BLACKLISTED_GUEST_EMAILS=
|
||||
|
||||
# Used to allow browser push notifications
|
||||
# You can use: 'npx web-push generate-vapid-keys' to generate these keys
|
||||
NEXT_PUBLIC_VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
|
||||
# Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs)
|
||||
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=
|
||||
NEXT_PUBLIC_WEBSITE_TERMS_URL=
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQ
|
||||
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useNotifications, ButtonState } from "@calcom/lib/hooks/useNotifications";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -144,6 +145,27 @@ export default function Bookings() {
|
||||
)[0] || [];
|
||||
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const { buttonToShow, isLoading, enableNotifications, disableNotifications } = useNotifications();
|
||||
|
||||
const actions = (
|
||||
<div>
|
||||
{buttonToShow && (
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={buttonToShow === ButtonState.ALLOW ? enableNotifications : disableNotifications}
|
||||
loading={isLoading}
|
||||
disabled={buttonToShow === ButtonState.DENIED}
|
||||
tooltipSide="bottom"
|
||||
tooltip={buttonToShow === ButtonState.DENIED ? t("you_have_denied_notifications") : undefined}>
|
||||
{t(
|
||||
buttonToShow === ButtonState.DISABLE
|
||||
? "disable_browser_notifications"
|
||||
: "allow_browser_notifications"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Shell
|
||||
@@ -152,7 +174,8 @@ export default function Bookings() {
|
||||
heading={t("bookings")}
|
||||
subtitle={t("bookings_description")}
|
||||
title="Bookings"
|
||||
description="Create events to share for people to book on your calendar.">
|
||||
description="Create events to share for people to book on your calendar."
|
||||
actions={actions}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row flex-wrap justify-between">
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
|
||||
+10
-3
@@ -1,6 +1,6 @@
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { AppContextType } from "next/dist/shared/lib/utils";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
@@ -11,8 +11,15 @@ import "../styles/globals.css";
|
||||
function MyApp(props: AppProps) {
|
||||
const { Component, pageProps } = props;
|
||||
|
||||
if (Component.PageWrapper !== undefined) return Component.PageWrapper(props);
|
||||
return <Component {...pageProps} />;
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && "serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/service-worker.js");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const content = Component.PageWrapper ? <Component.PageWrapper {...props} /> : <Component {...pageProps} />;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
@@ -461,6 +461,14 @@
|
||||
"dynamic_booking": "Dynamic group links",
|
||||
"allow_seo_indexing": "Allow search engines to access your public content",
|
||||
"seo_indexing": "Allow SEO Indexing",
|
||||
"allow_browser_notifications": "Allow Browser Notifications",
|
||||
"you_have_denied_notifications": "You have denied notifications. Reset permission in browser settings to enable.",
|
||||
"disable_browser_notifications": "Disable Browser Notifications",
|
||||
"browser_notifications_turned_on": "Browser Notifications turned on",
|
||||
"browser_notifications_turned_off": "Browser Notifications turned off",
|
||||
"browser_notifications_denied": "Browser Notifications denied",
|
||||
"please_allow_notifications": "Please allow notifications from the prompt",
|
||||
"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",
|
||||
"full_name": "Full name",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -18,9 +18,11 @@
|
||||
"react-select": "^5.7.0",
|
||||
"react-sticky-box": "^2.0.4",
|
||||
"react-virtual": "^2.10.4",
|
||||
"web-push": "^3.6.7",
|
||||
"zustand": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react-hooks": "^8.0.1"
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/web-push": "^3.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -331,9 +331,10 @@ model User {
|
||||
secondaryEmails SecondaryEmail[]
|
||||
isPlatformManaged Boolean @default(false)
|
||||
|
||||
OutOfOfficeReasons OutOfOfficeReason[]
|
||||
smsLockState SMSLockState @default(UNLOCKED)
|
||||
smsLockReviewedByAdmin Boolean @default(false)
|
||||
OutOfOfficeReasons OutOfOfficeReason[]
|
||||
smsLockState SMSLockState @default(UNLOCKED)
|
||||
smsLockReviewedByAdmin Boolean @default(false)
|
||||
NotificationsSubscriptions NotificationsSubscriptions[]
|
||||
|
||||
@@unique([email])
|
||||
@@unique([email, username])
|
||||
@@ -346,6 +347,15 @@ model User {
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model NotificationsSubscriptions {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
subscription String
|
||||
|
||||
@@index([userId, subscription])
|
||||
}
|
||||
|
||||
// It holds Organization Profiles as well as User Profiles for users that have been added to an organization
|
||||
model Profile {
|
||||
id Int @id @default(autoincrement())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import authedProcedure from "../../procedures/authedProcedure";
|
||||
import { importHandler, router } from "../../trpc";
|
||||
import { ZAddNotificationsSubscriptionInputSchema } from "./addNotificationsSubscription.schema";
|
||||
import { ZAddSecondaryEmailInputSchema } from "./addSecondaryEmail.schema";
|
||||
import { ZAppByIdInputSchema } from "./appById.schema";
|
||||
import { ZAppCredentialsByTypeInputSchema } from "./appCredentialsByType.schema";
|
||||
@@ -16,6 +17,7 @@ import { ZNoShowInputSchema } from "./markNoShow.schema";
|
||||
import { ZOutOfOfficeInputSchema, ZOutOfOfficeDelete } from "./outOfOffice.schema";
|
||||
import { me } from "./procedures/me";
|
||||
import { teamsAndUserProfilesQuery } from "./procedures/teamsAndUserProfilesQuery";
|
||||
import { ZRemoveNotificationsSubscriptionInputSchema } from "./removeNotificationsSubscription.schema";
|
||||
import { ZRoutingFormOrderInputSchema } from "./routingFormOrder.schema";
|
||||
import { ZSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
|
||||
import { ZSubmitFeedbackInputSchema } from "./submitFeedback.schema";
|
||||
@@ -59,6 +61,8 @@ type AppsRouterHandlerCache = {
|
||||
addSecondaryEmail?: typeof import("./addSecondaryEmail.handler").addSecondaryEmailHandler;
|
||||
getTravelSchedules?: typeof import("./getTravelSchedules.handler").getTravelSchedulesHandler;
|
||||
outOfOfficeReasonList?: typeof import("./outOfOfficeReasons.handler").outOfOfficeReasonList;
|
||||
addNotificationsSubscription?: typeof import("./addNotificationsSubscription.handler").addNotificationsSubscriptionHandler;
|
||||
removeNotificationsSubscription?: typeof import("./removeNotificationsSubscription.handler").removeNotificationsSubscriptionHandler;
|
||||
markNoShow?: typeof import("./markNoShow.handler").markNoShow;
|
||||
};
|
||||
|
||||
@@ -503,6 +507,38 @@ export const loggedInViewerRouter = router({
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.outOfOfficeReasonList();
|
||||
}),
|
||||
addNotificationsSubscription: authedProcedure
|
||||
.input(ZAddNotificationsSubscriptionInputSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.addNotificationsSubscription) {
|
||||
UNSTABLE_HANDLER_CACHE.addNotificationsSubscription = (
|
||||
await import("./addNotificationsSubscription.handler")
|
||||
).addNotificationsSubscriptionHandler;
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.addNotificationsSubscription) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.addNotificationsSubscription({ ctx, input });
|
||||
}),
|
||||
removeNotificationsSubscription: authedProcedure
|
||||
.input(ZRemoveNotificationsSubscriptionInputSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.removeNotificationsSubscription) {
|
||||
UNSTABLE_HANDLER_CACHE.removeNotificationsSubscription = (
|
||||
await import("./removeNotificationsSubscription.handler")
|
||||
).removeNotificationsSubscriptionHandler;
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.removeNotificationsSubscription) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.removeNotificationsSubscription({ ctx, input });
|
||||
}),
|
||||
markNoShow: authedProcedure.input(ZNoShowInputSchema).mutation(async (opts) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.markNoShow) {
|
||||
UNSTABLE_HANDLER_CACHE.markNoShow = (await import("./markNoShow.handler")).markNoShow;
|
||||
@@ -512,7 +548,6 @@ export const loggedInViewerRouter = router({
|
||||
if (!UNSTABLE_HANDLER_CACHE.markNoShow) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.markNoShow(opts);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import type { TAddNotificationsSubscriptionInputSchema } from "./addNotificationsSubscription.schema";
|
||||
|
||||
type AddSecondaryEmailOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TAddNotificationsSubscriptionInputSchema;
|
||||
};
|
||||
|
||||
export const addNotificationsSubscriptionHandler = async ({ ctx, input }: AddSecondaryEmailOptions) => {
|
||||
const { user } = ctx;
|
||||
const { subscription } = input;
|
||||
|
||||
const existingSubscription = await prisma.notificationsSubscriptions.findFirst({
|
||||
where: { userId: user.id, subscription },
|
||||
});
|
||||
|
||||
if (!existingSubscription) {
|
||||
await prisma.notificationsSubscriptions.create({
|
||||
data: { userId: user.id, subscription },
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Subscription added successfully",
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZAddNotificationsSubscriptionInputSchema = z.object({
|
||||
subscription: z.string(),
|
||||
});
|
||||
|
||||
export type TAddNotificationsSubscriptionInputSchema = z.infer<
|
||||
typeof ZAddNotificationsSubscriptionInputSchema
|
||||
>;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import type { TRemoveNotificationsSubscriptionInputSchema } from "./removeNotificationsSubscription.schema";
|
||||
|
||||
type AddSecondaryEmailOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TRemoveNotificationsSubscriptionInputSchema;
|
||||
};
|
||||
|
||||
export const removeNotificationsSubscriptionHandler = async ({ ctx, input }: AddSecondaryEmailOptions) => {
|
||||
const { user } = ctx;
|
||||
const { subscription } = input;
|
||||
|
||||
// We just use findFirst because there will only be single unique subscription for a user
|
||||
const subscriptionToDelete = await prisma.notificationsSubscriptions.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
subscription,
|
||||
},
|
||||
});
|
||||
|
||||
if (subscriptionToDelete) {
|
||||
await prisma.notificationsSubscriptions.delete({
|
||||
where: {
|
||||
id: subscriptionToDelete.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Subscription removed successfully",
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZRemoveNotificationsSubscriptionInputSchema = z.object({
|
||||
subscription: z.string(),
|
||||
});
|
||||
|
||||
export type TRemoveNotificationsSubscriptionInputSchema = z.infer<
|
||||
typeof ZRemoveNotificationsSubscriptionInputSchema
|
||||
>;
|
||||
+4
-1
@@ -42,6 +42,7 @@
|
||||
"STRIPE_TEAM_MONTHLY_PRICE_ID",
|
||||
"STRIPE_ORG_MONTHLY_PRICE_ID",
|
||||
"NEXT_PUBLIC_API_V2_URL",
|
||||
"NEXT_PUBLIC_VAPID_PUBLIC_KEY",
|
||||
"BUILD_STANDALONE"
|
||||
]
|
||||
},
|
||||
@@ -425,6 +426,8 @@
|
||||
"NEXT_PUBLIC_HEAD_SCRIPTS",
|
||||
"NEXT_PUBLIC_BODY_SCRIPTS",
|
||||
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
|
||||
"NEXT_PUBLIC_API_V2_ROOT_URL"
|
||||
"NEXT_PUBLIC_API_V2_ROOT_URL",
|
||||
"NEXT_PUBLIC_VAPID_PUBLIC_KEY",
|
||||
"VAPID_PRIVATE_KEY"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4123,11 +4123,13 @@ __metadata:
|
||||
"@lexical/react": ^0.9.0
|
||||
"@tanstack/react-table": ^8.9.3
|
||||
"@testing-library/react-hooks": ^8.0.1
|
||||
"@types/web-push": ^3.6.3
|
||||
framer-motion: ^10.12.8
|
||||
lexical: ^0.9.0
|
||||
react-select: ^5.7.0
|
||||
react-sticky-box: ^2.0.4
|
||||
react-virtual: ^2.10.4
|
||||
web-push: ^3.6.7
|
||||
zustand: ^4.3.2
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -15937,6 +15939,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/web-push@npm:^3.6.3":
|
||||
version: 3.6.3
|
||||
resolution: "@types/web-push@npm:3.6.3"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 6fc6ac93dbb87d99ae1d08f065d1c80620ac69d966796d358363849903aaf6dcbf222cf3914a05b92db542289618484ef8cf5d6b50d1737d56527e9f2c65f578
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/webidl-conversions@npm:*":
|
||||
version: 6.1.1
|
||||
resolution: "@types/webidl-conversions@npm:6.1.1"
|
||||
@@ -17761,7 +17772,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"asn1.js@npm:^5.2.0":
|
||||
"asn1.js@npm:^5.2.0, asn1.js@npm:^5.3.0":
|
||||
version: 5.4.1
|
||||
resolution: "asn1.js@npm:5.4.1"
|
||||
dependencies:
|
||||
@@ -26567,6 +26578,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http_ece@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "http_ece@npm:1.2.0"
|
||||
checksum: 8b716baa83eb985104abe60fd7acdc5198e3d626e9608bdfa1de62851b6b420d89735b483d51045688e14c8fa2031a8f182ac1067394db61c307c7e541983b38
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-browserify@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "https-browserify@npm:1.0.0"
|
||||
@@ -26624,6 +26642,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-proxy-agent@npm:^7.0.0":
|
||||
version: 7.0.5
|
||||
resolution: "https-proxy-agent@npm:7.0.5"
|
||||
dependencies:
|
||||
agent-base: ^7.0.2
|
||||
debug: 4
|
||||
checksum: 2e1a28960f13b041a50702ee74f240add8e75146a5c37fc98f1960f0496710f6918b3a9fe1e5aba41e50f58e6df48d107edd9c405c5f0d73ac260dabf2210857
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-proxy-agent@npm:^7.0.3":
|
||||
version: 7.0.4
|
||||
resolution: "https-proxy-agent@npm:7.0.4"
|
||||
@@ -42561,6 +42589,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-push@npm:^3.6.7":
|
||||
version: 3.6.7
|
||||
resolution: "web-push@npm:3.6.7"
|
||||
dependencies:
|
||||
asn1.js: ^5.3.0
|
||||
http_ece: 1.2.0
|
||||
https-proxy-agent: ^7.0.0
|
||||
jws: ^4.0.0
|
||||
minimist: ^1.2.5
|
||||
bin:
|
||||
web-push: src/cli.js
|
||||
checksum: 306862864675a28702f378c779dfc87f5f2d717fadbcb622c8b40231e0ec074a5592251cb937b96a7e85dcfeafd475d12f0ec9044be210134ad386872d8e56ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-streams-polyfill@npm:4.0.0-beta.1":
|
||||
version: 4.0.0-beta.1
|
||||
resolution: "web-streams-polyfill@npm:4.0.0-beta.1"
|
||||
|
||||
Reference in New Issue
Block a user