From aedc88f975ed84384ff64f33a8dde191b848ce58 Mon Sep 17 00:00:00 2001 From: Hitesh Sisara Date: Fri, 1 Mar 2024 01:28:46 +0530 Subject: [PATCH] feat: show top banner for broken integrations [Cal 3125] (#13720) * added InvalidAppCredentialBanner * Create InvalidAppCredentialsBanner.tsx * Update getUserTopBanners.handler.ts * Create checkForInvalidAppCredentials.ts * Update common.json * Update common.json * Update getUserTopBanners.handler.ts * Update checkForInvalidAppCredentials.ts * also included team's credentials * reverse yarnlock changes * fix .env * fixed types * chore: translation --------- Co-authored-by: Udit Takkar --- apps/web/public/static/locales/en/common.json | 3 +- packages/features/shell/Shell.tsx | 12 +++++ .../InvalidAppCredentialsBanner.tsx | 49 +++++++++++++++++ .../checkForInvalidAppCredentials.ts | 52 +++++++++++++++++++ .../getUserTopBanners.handler.ts | 24 ++++++--- 5 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 packages/features/users/components/InvalidAppCredentialsBanner.tsx create mode 100644 packages/trpc/server/routers/loggedInViewer/checkForInvalidAppCredentials.ts diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index ebbef8e401..039908543e 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1601,7 +1601,8 @@ "attendee_email_variable": "Attendee email", "attendee_email_info": "The person booking's email", "kbar_search_placeholder": "Type a command or search...", - "invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.", + "invalid_credential": "It looks like permissions expired or were revoked for {{appName}}.", + "invalid_credential_action": "Reinstall app", "reschedule_reason": "Reschedule reason", "choose_common_schedule_team_event": "Choose a common schedule", "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.", diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index c31f6a5554..a26db7f1d5 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -29,6 +29,10 @@ import AdminPasswordBanner, { import CalendarCredentialBanner, { type CalendarCredentialBannerProps, } from "@calcom/features/users/components/CalendarCredentialBanner"; +import { + InvalidAppCredentialBanners, + type InvalidAppCredentialBannersProps, +} from "@calcom/features/users/components/InvalidAppCredentialsBanner"; import UserV2OptInBanner from "@calcom/features/users/components/UserV2OptInBanner"; import VerifyEmailBanner, { type VerifyEmailBannerProps, @@ -163,6 +167,7 @@ type BannerTypeProps = { adminPasswordBanner: AdminPasswordBannerProps; impersonationBanner: ImpersonatingBannerProps; calendarCredentialBanner: CalendarCredentialBannerProps; + invalidAppCredentialBanners: InvalidAppCredentialBannersProps; }; type BannerType = keyof BannerTypeProps; @@ -178,6 +183,9 @@ const BannerComponent: BannerComponent = { adminPasswordBanner: (props: AdminPasswordBannerProps) => , impersonationBanner: (props: ImpersonatingBannerProps) => , calendarCredentialBanner: (props: CalendarCredentialBannerProps) => , + invalidAppCredentialBanners: (props: InvalidAppCredentialBannersProps) => ( + + ), }; function useRedirectToOnboardingIfNeeded() { @@ -255,6 +263,7 @@ const Layout = (props: LayoutProps) => { {/* todo: only run this if timezone is different */} +
{banners && (
@@ -278,6 +287,9 @@ const Layout = (props: LayoutProps) => { } else if (key === "calendarCredentialBanner") { const Banner = BannerComponent[key]; return ; + } else if (key === "invalidAppCredentialBanners") { + const Banner = BannerComponent[key]; + return ; } })}
diff --git a/packages/features/users/components/InvalidAppCredentialsBanner.tsx b/packages/features/users/components/InvalidAppCredentialsBanner.tsx new file mode 100644 index 0000000000..e97c708eee --- /dev/null +++ b/packages/features/users/components/InvalidAppCredentialsBanner.tsx @@ -0,0 +1,49 @@ +import { useRouter } from "next/navigation"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { type RouterOutputs } from "@calcom/trpc"; +import { TopBanner } from "@calcom/ui"; + +export type InvalidAppCredentialBannersProps = { + data: RouterOutputs["viewer"]["getUserTopBanners"]["invalidAppCredentialBanners"]; +}; + +export function InvalidAppCredentialBanners({ data }: InvalidAppCredentialBannersProps) { + if (data.length === 0) { + return null; // No need to show banner if the array is empty + } + + return ( +
+ {data.map((app) => ( + + ))} +
+ ); +} + +export type InvalidAppCredentialBannerProps = { + name: string; + slug: string; +}; + +export function InvalidAppCredentialBanner({ name, slug }: InvalidAppCredentialBannerProps) { + const { t } = useLocale(); + const router = useRouter(); + + const handleClick = () => { + router.push(`/apps/${slug}`); + }; + + return ( + + {t("invalid_credential_action")} + + } + /> + ); +} diff --git a/packages/trpc/server/routers/loggedInViewer/checkForInvalidAppCredentials.ts b/packages/trpc/server/routers/loggedInViewer/checkForInvalidAppCredentials.ts new file mode 100644 index 0000000000..edc095296a --- /dev/null +++ b/packages/trpc/server/routers/loggedInViewer/checkForInvalidAppCredentials.ts @@ -0,0 +1,52 @@ +import { getAppFromSlug } from "@calcom/app-store/utils"; +import { type InvalidAppCredentialBannerProps } from "@calcom/features/users/components/InvalidAppCredentialsBanner"; +import { prisma } from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/client"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +type checkInvalidAppCredentialsOptions = { + ctx: { + user: NonNullable; + }; +}; + +export const checkInvalidAppCredentials = async ({ ctx }: checkInvalidAppCredentialsOptions) => { + const userId = ctx.user.id; + + const apps = await prisma.credential.findMany({ + where: { + OR: [ + { + userId: userId, + }, + { + team: { + members: { + some: { + userId: userId, + accepted: true, + role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] }, + }, + }, + }, + }, + ], + invalid: true, + }, + select: { + appId: true, + }, + }); + + const appNamesAndSlugs: InvalidAppCredentialBannerProps[] = []; + for (const app of apps) { + if (app.appId) { + const appId = app.appId; + const appMeta = await getAppFromSlug(appId); + const name = appMeta ? appMeta.name : appId; + appNamesAndSlugs.push({ slug: appId, name }); + } + } + + return appNamesAndSlugs; +}; diff --git a/packages/trpc/server/routers/loggedInViewer/getUserTopBanners.handler.ts b/packages/trpc/server/routers/loggedInViewer/getUserTopBanners.handler.ts index c81a00452f..b9b3313c68 100644 --- a/packages/trpc/server/routers/loggedInViewer/getUserTopBanners.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/getUserTopBanners.handler.ts @@ -5,6 +5,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { checkIfOrgNeedsUpgradeHandler } from "../viewer/organizations/checkIfOrgNeedsUpgrade.handler"; import { getUpgradeableHandler } from "../viewer/teams/getUpgradeable.handler"; +import { checkInvalidAppCredentials } from "./checkForInvalidAppCredentials"; import { shouldVerifyEmailHandler } from "./shouldVerifyEmail.handler"; type Props = { @@ -38,14 +39,21 @@ export const getUserTopBannersHandler = async ({ ctx }: Props) => { const upgradeableOrgMememberships = checkIfOrgNeedsUpgradeHandler({ ctx }); const shouldEmailVerify = shouldVerifyEmailHandler({ ctx }); const isInvalidCalendarCredential = checkInvalidGoogleCalendarCredentials({ ctx }); + const appsWithInavlidCredentials = checkInvalidAppCredentials({ ctx }); - const [teamUpgradeBanner, orgUpgradeBanner, verifyEmailBanner, calendarCredentialBanner] = - await Promise.allSettled([ - upgradeableTeamMememberships, - upgradeableOrgMememberships, - shouldEmailVerify, - isInvalidCalendarCredential, - ]); + const [ + teamUpgradeBanner, + orgUpgradeBanner, + verifyEmailBanner, + calendarCredentialBanner, + invalidAppCredentialBanners, + ] = await Promise.allSettled([ + upgradeableTeamMememberships, + upgradeableOrgMememberships, + shouldEmailVerify, + isInvalidCalendarCredential, + appsWithInavlidCredentials, + ]); return { teamUpgradeBanner: teamUpgradeBanner.status === "fulfilled" ? teamUpgradeBanner.value : [], @@ -53,5 +61,7 @@ export const getUserTopBannersHandler = async ({ ctx }: Props) => { verifyEmailBanner: verifyEmailBanner.status === "fulfilled" ? !verifyEmailBanner.value.isVerified : false, calendarCredentialBanner: calendarCredentialBanner.status === "fulfilled" ? calendarCredentialBanner.value : false, + invalidAppCredentialBanners: + invalidAppCredentialBanners.status === "fulfilled" ? invalidAppCredentialBanners.value : [], }; };