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 <udit222001@gmail.com>
This commit is contained in:
Hitesh Sisara
2024-02-29 19:58:46 +00:00
committed by GitHub
co-authored by Udit Takkar
parent 67256b5b6e
commit aedc88f975
5 changed files with 132 additions and 8 deletions
@@ -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.",
+12
View File
@@ -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) => <AdminPasswordBanner {...props} />,
impersonationBanner: (props: ImpersonatingBannerProps) => <ImpersonatingBanner {...props} />,
calendarCredentialBanner: (props: CalendarCredentialBannerProps) => <CalendarCredentialBanner {...props} />,
invalidAppCredentialBanners: (props: InvalidAppCredentialBannersProps) => (
<InvalidAppCredentialBanners {...props} />
),
};
function useRedirectToOnboardingIfNeeded() {
@@ -255,6 +263,7 @@ const Layout = (props: LayoutProps) => {
{/* todo: only run this if timezone is different */}
<TimezoneChangeDialog />
<div className="flex min-h-screen flex-col">
{banners && (
<div className="sticky top-0 z-10 w-full divide-y divide-black">
@@ -278,6 +287,9 @@ const Layout = (props: LayoutProps) => {
} else if (key === "calendarCredentialBanner") {
const Banner = BannerComponent[key];
return <Banner data={banners[key]} key={key} />;
} else if (key === "invalidAppCredentialBanners") {
const Banner = BannerComponent[key];
return <Banner data={banners[key]} key={key} />;
}
})}
</div>
@@ -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 (
<div>
{data.map((app) => (
<InvalidAppCredentialBanner key={app.slug} name={app.name} slug={app.slug} />
))}
</div>
);
}
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 (
<TopBanner
text={` ${t("invalid_credential", { appName: name })} `}
variant="warning"
actions={
<button className="border-b border-b-black" onClick={handleClick}>
{t("invalid_credential_action")}
</button>
}
/>
);
}
@@ -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<TrpcSessionUser>;
};
};
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;
};
@@ -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 : [],
};
};