-
+ {banners && (
+
+ {Object.keys(banners).map((key) => {
+ if (key === "teamUpgradeBanner") {
+ const Banner = BannerComponent[key];
+ return ;
+ } else if (key === "orgUpgradeBanner") {
+ const Banner = BannerComponent[key];
+ return ;
+ } else if (key === "verifyEmailBanner") {
+ const Banner = BannerComponent[key];
+ return ;
+ } else if (key === "adminPasswordBanner") {
+ const Banner = BannerComponent[key];
+ return ;
+ } else if (key === "impersonationBanner") {
+ const Banner = BannerComponent[key];
+ return ;
+ } else if (key === "calendarCredentialBanner") {
+ const Banner = BannerComponent[key];
+ return ;
+ }
+ })}
+
+ )}
+
{props.SidebarContainer ? (
cloneElement(props.SidebarContainer, { bannersHeight })
diff --git a/packages/features/users/components/AdminPasswordBanner.tsx b/packages/features/users/components/AdminPasswordBanner.tsx
index 3ac73bd15d..7c01f1d9f4 100644
--- a/packages/features/users/components/AdminPasswordBanner.tsx
+++ b/packages/features/users/components/AdminPasswordBanner.tsx
@@ -1,12 +1,13 @@
-import { useSession } from "next-auth/react";
+import type { SessionContextValue } from "next-auth/react";
import Link from "next/link";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TopBanner } from "@calcom/ui";
-function AdminPasswordBanner() {
+export type AdminPasswordBannerProps = { data: SessionContextValue["data"] };
+
+function AdminPasswordBanner({ data }: AdminPasswordBannerProps) {
const { t } = useLocale();
- const { data } = useSession();
if (data?.user.role !== "INACTIVE_ADMIN") return null;
diff --git a/packages/features/users/components/CalendarCredentialBanner.tsx b/packages/features/users/components/CalendarCredentialBanner.tsx
new file mode 100644
index 0000000000..c20448d78b
--- /dev/null
+++ b/packages/features/users/components/CalendarCredentialBanner.tsx
@@ -0,0 +1,31 @@
+import Link from "next/link";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { type RouterOutputs } from "@calcom/trpc";
+import { TopBanner } from "@calcom/ui";
+
+export type CalendarCredentialBannerProps = {
+ data: RouterOutputs["viewer"]["getUserTopBanners"]["calendarCredentialBanner"];
+};
+
+function CalendarCredentialBanner({ data }: CalendarCredentialBannerProps) {
+ const { t } = useLocale();
+
+ if (!data) return null;
+
+ return (
+ <>
+
+ {t("check_here")}
+
+ }
+ />
+ >
+ );
+}
+
+export default CalendarCredentialBanner;
diff --git a/packages/features/users/components/VerifyEmailBanner.tsx b/packages/features/users/components/VerifyEmailBanner.tsx
index cc8aebbff1..cdbf9125ff 100644
--- a/packages/features/users/components/VerifyEmailBanner.tsx
+++ b/packages/features/users/components/VerifyEmailBanner.tsx
@@ -1,23 +1,21 @@
-import { useSession } from "next-auth/react";
-
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
-import useEmailVerifyCheck from "@calcom/trpc/react/hooks/useEmailVerifyCheck";
import { TopBanner, showToast } from "@calcom/ui";
import { Mail } from "@calcom/ui/components/icon";
import { useFlagMap } from "../../flags/context/provider";
-function VerifyEmailBanner() {
+export type VerifyEmailBannerProps = {
+ data: boolean;
+};
+
+function VerifyEmailBanner({ data }: VerifyEmailBannerProps) {
const flags = useFlagMap();
const { t } = useLocale();
- const { data, isLoading } = useEmailVerifyCheck();
const mutation = trpc.viewer.auth.resendVerifyEmail.useMutation();
- const session = useSession();
- const isLoggedIn = session?.data?.user;
- if (!isLoggedIn || isLoading || data?.isVerified || !flags["email-verification"]) return null;
+ if (!data || !flags["email-verification"]) return null;
return (
<>
diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts
index 25cd09580c..c1fa5a6381 100644
--- a/packages/lib/constants.ts
+++ b/packages/lib/constants.ts
@@ -108,6 +108,8 @@ export const APP_CREDENTIAL_SHARING_ENABLED =
export const DEFAULT_LIGHT_BRAND_COLOR = "#292929";
export const DEFAULT_DARK_BRAND_COLOR = "#fafafa";
+export const TOP_BANNER_HEIGHT = 40;
+
const defaultOnNaN = (testedValue: number, defaultValue: number) =>
!Number.isNaN(testedValue) ? testedValue : defaultValue;
diff --git a/packages/trpc/server/routers/loggedInViewer/_router.tsx b/packages/trpc/server/routers/loggedInViewer/_router.tsx
index 1c96204997..5dc20024bb 100644
--- a/packages/trpc/server/routers/loggedInViewer/_router.tsx
+++ b/packages/trpc/server/routers/loggedInViewer/_router.tsx
@@ -44,6 +44,7 @@ type AppsRouterHandlerCache = {
getUsersDefaultConferencingApp?: typeof import("./getUsersDefaultConferencingApp.handler").getUsersDefaultConferencingAppHandler;
updateUserDefaultConferencingApp?: typeof import("./updateUserDefaultConferencingApp.handler").updateUserDefaultConferencingAppHandler;
teamsAndUserProfilesQuery?: typeof import("./teamsAndUserProfilesQuery.handler").teamsAndUserProfilesQuery;
+ getUserTopBanners?: typeof import("./getUserTopBanners.handler").getUserTopBannersHandler;
};
const UNSTABLE_HANDLER_CACHE: AppsRouterHandlerCache = {};
@@ -340,6 +341,21 @@ export const loggedInViewerRouter = router({
return UNSTABLE_HANDLER_CACHE.getCalVideoRecordings({ ctx, input });
}),
+ getUserTopBanners: authedProcedure.query(async ({ ctx }) => {
+ if (!UNSTABLE_HANDLER_CACHE.getUserTopBanners) {
+ UNSTABLE_HANDLER_CACHE.getUserTopBanners = (
+ await import("./getUserTopBanners.handler")
+ ).getUserTopBannersHandler;
+ }
+
+ // Unreachable code but required for type safety
+ if (!UNSTABLE_HANDLER_CACHE.getUserTopBanners) {
+ throw new Error("Failed to load handler");
+ }
+
+ return UNSTABLE_HANDLER_CACHE.getUserTopBanners({ ctx });
+ }),
+
getDownloadLinkOfCalVideoRecordings: authedProcedure
.input(ZGetDownloadLinkOfCalVideoRecordingsInputSchema)
.query(async ({ ctx, input }) => {
diff --git a/packages/trpc/server/routers/loggedInViewer/getUserTopBanners.handler.ts b/packages/trpc/server/routers/loggedInViewer/getUserTopBanners.handler.ts
new file mode 100644
index 0000000000..c81a00452f
--- /dev/null
+++ b/packages/trpc/server/routers/loggedInViewer/getUserTopBanners.handler.ts
@@ -0,0 +1,57 @@
+import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
+import { prisma } from "@calcom/prisma";
+import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
+import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
+
+import { checkIfOrgNeedsUpgradeHandler } from "../viewer/organizations/checkIfOrgNeedsUpgrade.handler";
+import { getUpgradeableHandler } from "../viewer/teams/getUpgradeable.handler";
+import { shouldVerifyEmailHandler } from "./shouldVerifyEmail.handler";
+
+type Props = {
+ ctx: {
+ user: NonNullable;
+ };
+};
+
+const checkInvalidGoogleCalendarCredentials = async ({ ctx }: Props) => {
+ const userCredentials = await prisma.credential.findMany({
+ where: {
+ userId: ctx.user.id,
+ type: "google_calendar",
+ },
+ select: credentialForCalendarServiceSelect,
+ });
+
+ const calendarCredentials = getCalendarCredentials(userCredentials);
+
+ const { connectedCalendars } = await getConnectedCalendars(
+ calendarCredentials,
+ ctx.user.selectedCalendars,
+ ctx.user.destinationCalendar?.externalId
+ );
+
+ return connectedCalendars.some((calendar) => !!calendar.error);
+};
+
+export const getUserTopBannersHandler = async ({ ctx }: Props) => {
+ const upgradeableTeamMememberships = getUpgradeableHandler({ ctx });
+ const upgradeableOrgMememberships = checkIfOrgNeedsUpgradeHandler({ ctx });
+ const shouldEmailVerify = shouldVerifyEmailHandler({ ctx });
+ const isInvalidCalendarCredential = checkInvalidGoogleCalendarCredentials({ ctx });
+
+ const [teamUpgradeBanner, orgUpgradeBanner, verifyEmailBanner, calendarCredentialBanner] =
+ await Promise.allSettled([
+ upgradeableTeamMememberships,
+ upgradeableOrgMememberships,
+ shouldEmailVerify,
+ isInvalidCalendarCredential,
+ ]);
+
+ return {
+ teamUpgradeBanner: teamUpgradeBanner.status === "fulfilled" ? teamUpgradeBanner.value : [],
+ orgUpgradeBanner: orgUpgradeBanner.status === "fulfilled" ? orgUpgradeBanner.value : [],
+ verifyEmailBanner: verifyEmailBanner.status === "fulfilled" ? !verifyEmailBanner.value.isVerified : false,
+ calendarCredentialBanner:
+ calendarCredentialBanner.status === "fulfilled" ? calendarCredentialBanner.value : false,
+ };
+};
diff --git a/packages/ui/components/top-banner/TopBanner.tsx b/packages/ui/components/top-banner/TopBanner.tsx
index 2ab3155936..69646b1441 100644
--- a/packages/ui/components/top-banner/TopBanner.tsx
+++ b/packages/ui/components/top-banner/TopBanner.tsx
@@ -1,6 +1,7 @@
import classNames from "classnames";
import type { ComponentType, ReactNode } from "react";
+import { TOP_BANNER_HEIGHT } from "@calcom/lib/constants";
import type { LucideIcon, LucideProps } from "@calcom/ui/components/icon";
import { AlertTriangle, Info } from "@calcom/ui/components/icon";
@@ -40,8 +41,9 @@ export function TopBanner(props: TopBannerProps) {
return (