Fix FLUC on initial load

This commit is contained in:
Fergal Moran
2023-02-28 12:51:42 +00:00
parent 433180c39e
commit 9b5f9d986e
25 changed files with 978 additions and 162 deletions

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{theme-change}" />
</component>
</project>

1
.idea/web.iml generated
View File

@@ -11,5 +11,6 @@
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="theme-change" level="application" />
</component> </component>
</module> </module>

View File

@@ -27,6 +27,7 @@
"eslint-config-next": "13.1.5", "eslint-config-next": "13.1.5",
"feather-icons": "^4.29.0", "feather-icons": "^4.29.0",
"firebase": "^9.17.1", "firebase": "^9.17.1",
"firebase-admin": "^11.5.0",
"firebase-functions": "^4.2.1", "firebase-functions": "^4.2.1",
"fireschema": "^4.0.4", "fireschema": "^4.0.4",
"localforage": "^1.10.0", "localforage": "^1.10.0",
@@ -36,6 +37,8 @@
"pino": "^8.11.0", "pino": "^8.11.0",
"pino-logflare": "^0.3.12", "pino-logflare": "^0.3.12",
"react": "18.2.0", "react": "18.2.0",
"react-daisyui": "^3.0.3",
"react-device-detect": "^2.2.3",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-feather": "^2.0.10", "react-feather": "^2.0.10",

View File

@@ -1,4 +1,4 @@
import { LoginPage } from "@/components/auth"; import { LoginPage } from "@/components/pages/auth";
import React from "react"; import React from "react";
const Login = async () => { const Login = async () => {

View File

@@ -1,7 +1,8 @@
import React from "react"; import React from "react";
import { SignupPage } from "@/components/pages/auth";
const Signup = async () => { const Signup = async () => {
return <h1>Coming soon</h1>; return <div className="flex flex-wrap w-full justify-evenly"><SignupPage /></div>;
}; };
export default Signup; export default Signup;

View File

@@ -1,4 +1,7 @@
import React from "react";
export default function Head() { export default function Head() {
return ( return (
<> <>
<title>Radio::Otherway</title> <title>Radio::Otherway</title>
@@ -27,6 +30,8 @@ export default function Head() {
/> />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
{/*need to include this here to avoid the FLUC on initial load*/}
<script src="https://unpkg.com/theme-change@2.2.0/index.js" />
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
</> </>

View File

@@ -4,41 +4,38 @@ import "./globals.css";
import { Raleway } from "@next/font/google"; import { Raleway } from "@next/font/google";
import { NavBar, PushNotificationWrapper } from "@/components/layout"; import { NavBar, PushNotificationWrapper } from "@/components/layout";
import { AuthUserProvider } from "@/lib/auth/authUserContext"; import { AuthUserProvider } from "@/lib/auth/authUserContext";
import { Toaster } from "react-hot-toast"; import { themeChange } from "theme-change";
import { defaults } from "@/lib/constants";
const font = Raleway({ const font = Raleway({
weight: ["400", "700"], weight: ["400", "700"],
subsets: ["latin"], subsets: ["latin"],
variable: "--font-raleway", variable: "--font-raleway"
}); });
export default function RootLayout({ export default function RootLayout({
children, children
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
React.useEffect(() => { React.useEffect(() => {
const theme = localStorage.getItem("theme") || defaults.defaultTheme; themeChange(false);
if (theme && !document.body.dataset.theme) {
document.body.dataset.theme = theme;
}
}, []); }, []);
return ( return (
<html lang="en"> <html lang="en">
<head /> <head />
<body className={`${font.className}`}> <body className={`${font.className}`}>
<PushNotificationWrapper> <AuthUserProvider>
<AuthUserProvider> <PushNotificationWrapper>
<div className="flex flex-col min-h-screen bg-base-100"> <div className="flex flex-col min-h-screen bg-base-100">
<NavBar /> <NavBar />
<div className="items-end grow place-items-center bg-base-200 text-primary-content"> <div className="items-end grow place-items-center bg-base-200 text-primary-content">
<main className=" text-base-content">{children}</main> <main className=" text-base-content">{children}</main>
</div> </div>
</div> </div>
</AuthUserProvider> </PushNotificationWrapper>
</PushNotificationWrapper> </AuthUserProvider>
</body> </body>
</html> </html>
); );
} }

View File

@@ -1,3 +0,0 @@
import LoginPage from "./LoginPage";
export { LoginPage };

View File

@@ -38,9 +38,9 @@ const Navbar = () => {
<Link <Link
href="/signup" href="/signup"
id="signup" id="signup"
className="font-normal normal-case font-body btn-primary btn-sm btn" className="gap-1 normal-case btn-ghost btn"
> >
<PlusSquare size={12} className="mr-2" /> <PlusSquare size={20} className="inline-block w-5 h-5 stroke-current md:h-6 md:w-6" />
Register Register
</Link> </Link>
<Link <Link
@@ -62,10 +62,10 @@ const Navbar = () => {
<Link href="/"> <Link href="/">
<Image src="/logo.png" alt="Otherway" width={42} height={42} /> <Image src="/logo.png" alt="Otherway" width={42} height={42} />
</Link> </Link>
<div className="flex-col hidden ml-auto text-sm text-center font-body md:flex md:flex-row md:space-x-10"> <div className="flex-col hidden ml-auto text-sm text-center font-body md:flex md:flex-row">
{!loading && NavMenu} {!loading && NavMenu}
<ThemeSelector />
</div> </div>
<ThemeSelector />
<div className="ml-auto lg:hidden"> <div className="ml-auto lg:hidden">
<div className="dropdown-end dropdown" data-cy="dropdown"> <div className="dropdown-end dropdown" data-cy="dropdown">

View File

@@ -6,58 +6,92 @@ import { Toaster } from "react-hot-toast";
import { getMessaging, onMessage } from "firebase/messaging"; import { getMessaging, onMessage } from "firebase/messaging";
import { app } from "@/lib/auth/firebase"; import { app } from "@/lib/auth/firebase";
import ToastService from "../widgets/toast"; import ToastService from "../widgets/toast";
import { doc, setDoc } from "@firebase/firestore";
import { useAuthUserContext } from "@/lib/auth/authUserContext";
import { users } from "@/lib/db";
import logger from "@/lib/util/logging";
import { parseUserAgent } from "react-device-detect";
const PushNotificationWrapper = ({ children }: React.PropsWithChildren) => { const PushNotificationWrapper = ({ children }: React.PropsWithChildren) => {
const router = useRouter(); const router = useRouter();
useEffect(() => { const { profile } = useAuthUserContext();
setToken(); useEffect(() => {
const _getAndStoreRegistrationToken = async () => {
const { ua } = parseUserAgent(window.navigator.userAgent);
// Event listener that listens for the push notification event in the background if (!profile) return;
if ("serviceWorker" in navigator) { await setToken();
navigator.serviceWorker.addEventListener("message", (event) => {
console.log("event for the service worker", event); // Event listener that listens for the push notification event in the background
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", (event) => {
console.log("event for the service worker", event);
});
}
// Calls the getMessage() function if the token is there
async function setToken() {
try {
if (!profile) return;
const token = await firebaseCloudMessaging.init();
if (token && profile) {
const newRegistration = {
fcmToken: token,
deviceType: ua,
lastSeen: new Date()
};
const index = profile.deviceRegistrations?.findIndex(reg => {
return reg.fcmToken === token;
});
if (index !== undefined && index !== -1) {
if (profile.deviceRegistrations && profile.deviceRegistrations[index]) {
profile.deviceRegistrations[index] = newRegistration;
}
} else {
profile.deviceRegistrations?.push(newRegistration);
}
}
const profileWithRegistrations = Object.assign({}, profile);
await setDoc(doc(users, profile?.id), profileWithRegistrations, { merge: true });
getMessage();
} catch (error) {
console.log(error);
}
}
};
_getAndStoreRegistrationToken()
.catch(err => {
logger.error("PushNotificationWrapper", "_getAndStoreRegistrationToken_error", err);
});
}, [profile]);
function getMessage() {
const messaging = getMessaging(app);
onMessage(messaging, (message) => {
ToastService.custom(
<div
onClick={() =>
message?.data?.url &&
handleClickPushNotification(message?.data?.url)
}
>
<h5>{message?.notification?.title}</h5>
<h6>{message?.notification?.body}</h6>
</div>
);
}); });
} }
// Calls the getMessage() function if the token is there const handleClickPushNotification = (url: string) => {
async function setToken() { router.push(url);
try { };
const token = await firebaseCloudMessaging.init(); return (
if (token) { <>
console.log("token", token);
getMessage();
}
} catch (error) {
console.log(error);
}
}
});
function getMessage() {
const messaging = getMessaging(app);
onMessage(messaging, (message) => {
ToastService.custom(
<div
onClick={() =>
message?.data?.url &&
handleClickPushNotification(message?.data?.url)
}
>
<h5>{message?.notification?.title}</h5>
<h6>{message?.notification?.body}</h6>
</div>
);
});
}
const handleClickPushNotification = (url: string) => {
router.push(url);
};
return (
<>
<Toaster /> <Toaster />
{children} {children}
</> </>
); );
}; }
;
export default PushNotificationWrapper; export default PushNotificationWrapper;

View File

@@ -0,0 +1,157 @@
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import useFirebaseAuth from "@/lib/auth/useFirebaseAuth";
import { IoLogoFacebook, IoLogoGoogle, IoLogoTwitter } from "react-icons/io";
const SignupPage = () => {
const { signInWithGoogle, signInWithFacebook, signInWithTwitter, profile, signUp } =
useFirebaseAuth();
const router = useRouter();
const [error, setError] = React.useState("");
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [confirmPassword, setConfirmPassword] = React.useState("");
const register = async (
$event: React.SyntheticEvent<HTMLButtonElement>
): Promise<void> => {
$event.preventDefault();
const result = await signUp(email, password);
if (result === "auth/email-already-in-use") {
setError("This email address has already been used to create an account.");
} else if (result === "auth/invalid-email") {
setError("Please enter a correct email address");
} else {
setError("Unable to create an account for you at this time");
}
};
return (
<div className="max-w-lg p-10 rounded-md shadow-md font-body bg-base-100 text-base-content md:flex-1">
<h3 className="my-4 text-2xl font-semibold font-title">
Create New Account
</h3>
{error && (
<div className="shadow-lg alert alert-error mb-4">
<div>
<svg xmlns="http://www.w3.org/2000/svg" className="flex-shrink-0 w-6 h-6 stroke-current" fill="none"
viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{error}</span>
</div>
</div>
)}
<form action="#" className="flex flex-col space-y-5">
<div className="flex flex-col space-y-1">
<label htmlFor="email" className="text-sm">
Email address
</label>
<input
type="email"
id="email"
autoFocus
className="input-bordered input-primary input input-sm"
value={email}
onChange={(event) => {
setEmail(event.target.value);
}}
/>
</div>
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-between">
<label htmlFor="password" className="text-sm">
Password
</label>
</div>
<input
type="password"
id="password"
className="input-bordered input-primary input input-sm"
value={password}
onChange={(event) => {
setPassword(event.target.value);
}}
/>
</div>
<div className="flex flex-col space-y-1">
<div className="flex items-center justify-between">
<label htmlFor="password" className="text-sm">
Repeat password
</label>
</div>
<input
type="password"
id="password"
className="input-bordered input-primary input input-sm"
value={confirmPassword}
onChange={(event) => {
setConfirmPassword(event.target.value);
}}
/>
</div>
<div>
<button
className="w-full btn-primary btn"
onClick={(event) => {
void register(event);
}}
>
Register for Account
</button>
</div>
<div className="flex flex-col space-y-5">
<span className="flex items-center justify-center space-x-2">
<span className="h-px bg-gray-400 w-14" />
<span className="font-normal text-gray-500">or sign up with</span>
<span className="h-px bg-gray-400 w-14" />
</span>
<div className="flex items-center justify-center gap-4">
<button
type="button"
className="w-1/3 gap-2 btn"
onClick={signInWithTwitter}
>
<div className="text-base-content">
<IoLogoTwitter />
</div>
<span className="text-sm font-medium text-base-content">
Twitter
</span>
</button>
<button
type="button"
className="w-1/3 gap-2 btn"
onClick={signInWithGoogle}
>
<div className="text-base-content">
<IoLogoGoogle />
</div>
<span className="text-sm font-medium text-base-content">
Gmail
</span>
</button>
<button
type="button"
className="w-1/3 gap-2 btn"
onClick={signInWithFacebook}
>
<div className="text-base-content">
<IoLogoFacebook />
</div>
<span className="text-sm font-medium text-base-content">
Facebook
</span>
</button>
</div>
</div>
</form>
</div>
);
};
export default SignupPage;

View File

@@ -0,0 +1,4 @@
import LoginPage from "./LoginPage";
import SignupPage from "./SignupPage";
export { LoginPage, SignupPage };

View File

@@ -19,8 +19,7 @@ const ProfilePageComponentProfile = () => {
const [url, setUrl] = React.useState(""); const [url, setUrl] = React.useState("");
const [image, setImage] = React.useState(""); const [image, setImage] = React.useState("");
React.useEffect(() => { React.useEffect(() => {
console.log("ProfilePageComponentProfile", "useEffect", profile); if (profile) {
if (profile) {
setEmail(profile.email as string); setEmail(profile.email as string);
setDisplayName(profile.displayName as string); setDisplayName(profile.displayName as string);
setAbout(profile.about as string); setAbout(profile.about as string);

View File

@@ -5,21 +5,11 @@ import { useState, useEffect } from "react";
import { IoColorPaletteOutline } from "react-icons/io5"; import { IoColorPaletteOutline } from "react-icons/io5";
import { BiDownArrow } from "react-icons/bi"; import { BiDownArrow } from "react-icons/bi";
import { defaults } from "@/lib/constants"; import { defaults } from "@/lib/constants";
import { useTheme } from "react-daisyui";
import { themeChange } from "theme-change";
const ThemeSelector = () => { const ThemeSelector = () => {
const [activeTheme, setActiveTheme] = useState( const _switchTheme = useCallback(() => {
typeof window !== "undefined" && localStorage.getItem("theme") ||
defaults.defaultTheme);
//
useEffect(() => {
if (document) {
document.body.dataset.theme = activeTheme;
window.localStorage.setItem("theme", activeTheme);
}
}, [activeTheme]);
const _switchTheme = useCallback((theme: string) => {
setActiveTheme(theme);
const elem = document.activeElement as HTMLElement; const elem = document.activeElement as HTMLElement;
elem?.blur(); elem?.blur();
}, []); }, []);
@@ -39,7 +29,8 @@ const ThemeSelector = () => {
<button <button
key={theme.id} key={theme.id}
className="overflow-hidden text-left rounded-lg outline-base-content" className="overflow-hidden text-left rounded-lg outline-base-content"
onClick={() => _switchTheme(theme.id)} data-set-theme={theme.id}
onClick={() => _switchTheme()}
> >
<div <div
data-theme={theme.id} data-theme={theme.id}

View File

@@ -0,0 +1,8 @@
import admin from "firebase-admin";
const serviceAccount = JSON.parse(
process.env.FIREBASE_SERVICE_ACCOUNT_KEY as string
);
export const firebaseAdmin = admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});

View File

@@ -3,16 +3,16 @@ import localforage from "localforage";
import { getMessaging, getToken } from "firebase/messaging"; import { getMessaging, getToken } from "firebase/messaging";
import { app } from "./firebase"; import { app } from "./firebase";
const firebaseCloudMessaging = { const firebaseCloudMessaging = {
init: async () => { init: async (): Promise<string | null> => {
try { try {
const messaging = getMessaging(app); const messaging = getMessaging(app);
const tokenInLocalForage = await localforage.getItem("fcm_token"); const tokenInLocalForage = await localforage.getItem("fcm_token");
// Return the token if it is alredy in our local storage // Return the token if it is alredy in our local storage
if (tokenInLocalForage !== null) { if (tokenInLocalForage !== null) {
return tokenInLocalForage; return tokenInLocalForage as string;
} }
// Request the push notification permission from browser // Request the push notification permission from browser
@@ -23,14 +23,14 @@ const firebaseCloudMessaging = {
// Set token in our local storage // Set token in our local storage
if (token) { if (token) {
localforage.setItem("fcm_token", token); await localforage.setItem("fcm_token", token);
return token; return token;
} }
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return null;
} }
}, return null;
}
}; };
export { firebaseCloudMessaging }; export { firebaseCloudMessaging };

View File

@@ -19,6 +19,7 @@ import { Profile } from "@/models";
import { users } from "../db"; import { users } from "../db";
import { doc, getDoc, setDoc } from "firebase/firestore"; import { doc, getDoc, setDoc } from "firebase/firestore";
import { servicesVersion } from "@ts-morph/common/lib/typescript"; import { servicesVersion } from "@ts-morph/common/lib/typescript";
import logger from "../util/logging";
export default function useFirebaseAuth() { export default function useFirebaseAuth() {
const [profile, setProfile] = useState<Profile | undefined>(); const [profile, setProfile] = useState<Profile | undefined>();
@@ -38,7 +39,9 @@ export default function useFirebaseAuth() {
(savedProfile?.email || auth.currentUser.email) as string, (savedProfile?.email || auth.currentUser.email) as string,
(savedProfile?.displayName || auth.currentUser.email) as string, (savedProfile?.displayName || auth.currentUser.email) as string,
(savedProfile?.photoURL || auth.currentUser.email) as string, (savedProfile?.photoURL || auth.currentUser.email) as string,
savedProfile?.about as string savedProfile?.about as string,
new Date(),
savedProfile?.deviceRegistrations
); );
setProfile(profile); setProfile(profile);
await setDoc(doc(users, auth.currentUser.uid), Object.assign({}, profile), { await setDoc(doc(users, auth.currentUser.uid), Object.assign({}, profile), {
@@ -65,8 +68,16 @@ export default function useFirebaseAuth() {
const signIn = (email: string, password: string) => const signIn = (email: string, password: string) =>
signInWithEmailAndPassword(auth, email, password); signInWithEmailAndPassword(auth, email, password);
const signUp = (email: string, password: string) => const signUp = async (email: string, password: string): Promise<string> => {
createUserWithEmailAndPassword(auth, email, password); try {
const response = await createUserWithEmailAndPassword(auth, email, password);
logger.debug("useFireBaseAuth", "signUp_success", response);
return "";
} catch (err: { code: string } | any) {
logger.error("useFireBaseAuth", "signUp", err);
return err.code;
}
};
const logOut = () => signOut(auth).then(clear); const logOut = () => signOut(auth).then(clear);

View File

@@ -0,0 +1,5 @@
export default interface DeviceRegistration {
deviceType: string;
fcmToken: string;
lastSeen: Date;
};

View File

@@ -2,6 +2,7 @@ import Profile from "./profile";
import Show from "./show"; import Show from "./show";
import Reminder from "./reminder"; import Reminder from "./reminder";
import Notification from "./notification"; import Notification from "./notification";
import DeviceRegistration from "./deviceregistration";
import type { RemindersProcessed } from "./processes"; import type { RemindersProcessed } from "./processes";
export { export {
@@ -9,5 +10,6 @@ export {
Show, Show,
Reminder, Reminder,
Notification, Notification,
type DeviceRegistration,
RemindersProcessed RemindersProcessed
}; };

View File

@@ -1,3 +1,5 @@
import DeviceRegistration from "@/models/deviceregistration";
export default class Profile { export default class Profile {
id: string; id: string;
email: string | null; email: string | null;
@@ -6,6 +8,7 @@ export default class Profile {
emailVerified: boolean = false; emailVerified: boolean = false;
about?: String; about?: String;
lastSeen: Date; lastSeen: Date;
deviceRegistrations?: DeviceRegistration[] = [];
constructor( constructor(
id: string, id: string,
@@ -13,7 +16,8 @@ export default class Profile {
displayName: string | null, displayName: string | null,
photoURL: string | null, photoURL: string | null,
about?: string, about?: string,
lastSeen?: Date lastSeen?: Date,
deviceRegistrations?: DeviceRegistration[]
) { ) {
this.id = id; this.id = id;
this.email = email; this.email = email;
@@ -22,5 +26,6 @@ export default class Profile {
this.about = about || ""; this.about = about || "";
this.lastSeen = lastSeen || new Date(); this.lastSeen = lastSeen || new Date();
this.deviceRegistrations = deviceRegistrations || this.deviceRegistrations;
} }
} }

View File

@@ -1,23 +0,0 @@
import { getMessaging } from "firebase/messaging";
import { NextApiRequest, NextApiResponse } from "next";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const message = {
data: {
score: "850",
time: "2:45",
},
token:
"eQM4lB5_AWPgTPfIqdJ4jQ:APA91bHwBiljwSbyx7gU_IPLu59hFPkCPd-0OElLNQyF4m6nBluXfBnyvh115tpSjR0ceO9IhY7PuwkoR09u_JykBmjBvrsuUkDWzOdKxVLnV5STOV98dr56Avo5HdmPrei6SxMbHeTb",
};
// Send a message to the device corresponding to the provided
// registration token.
getMessaging()
.send(message)
.then((response) => {
console.log("Successfully sent message:", response);
})
.catch((error) => {
console.error("Error sending message:", error);
});
};

View File

@@ -0,0 +1,47 @@
import { NextApiRequest, NextApiResponse } from "next";
import logger from "@/lib/util/logging";
import { firebaseAdmin } from "@/lib/auth/firebaseAdmin";
import { doc, getDocs, query, where } from "@firebase/firestore";
import { getDoc } from "firebase/firestore";
import { users } from "@/lib/db";
import { Profile, Reminder } from "@/models";
const _getPayload = (message: string) => ({
notification: {
title: "Argle Bargle",
body: `You are a ${message}`,
image: "https://otherway.fergl.ie/logo.png"
},
data: {
url: "https://google.com"
}
});
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Send a message to the device corresponding to the provided
// registration token.
try {
const results: any[] = [];
const userResultSet = await getDocs(query(users, where("email", "==", "fergal.moran@gmail.com")));
for (let u of userResultSet.docs) {
const user = u.data() as Profile;
if (user?.deviceRegistrations) {
for (const token of user?.deviceRegistrations) {
results.push(await firebaseAdmin
.messaging()
.sendToDevice(
token.fcmToken,
_getPayload(token.deviceType)));
}
}
}
res.status(200).json({ results });
} catch (err) {
logger.error("fcm", "sendPush", "Failed", err);
res.status(500);
}
res.end();
};
export default handler;

View File

@@ -2,7 +2,6 @@ import { NextApiRequest, NextApiResponse } from "next";
import { setupCalendarWebhook } from "@/lib/util/google/calendarWatcher"; import { setupCalendarWebhook } from "@/lib/util/google/calendarWatcher";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await setupCalendarWebhook();
res.status(200).json({ hello: process.env }); res.status(200).json({ hello: process.env });
}; };
export default handler; export default handler;

639
yarn.lock

File diff suppressed because it is too large Load Diff