diff --git a/apps/web/lib/booking.ts b/apps/web/lib/booking.ts index 78f712e0c6..78f75df49d 100644 --- a/apps/web/lib/booking.ts +++ b/apps/web/lib/booking.ts @@ -70,6 +70,11 @@ export const getEventTypesFromDB = async (id: number) => { slug: true, name: true, hideBranding: true, + parent: { + select: { + hideBranding: true, + }, + }, }, }, workflows: { diff --git a/apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx b/apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx index 31cbed2209..9d0d01a2cb 100644 --- a/apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx +++ b/apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx @@ -6,6 +6,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking"; import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { shouldHideBrandingForTeamEvent, shouldHideBrandingForUserEvent } from "@calcom/lib/hideBranding"; import { EventRepository } from "@calcom/lib/server/repository/event"; import { UserRepository } from "@calcom/lib/server/repository/user"; import slugify from "@calcom/lib/slugify"; @@ -49,6 +50,11 @@ async function getUserPageProps(context: GetServerSidePropsContext) { id: true, slug: true, hideBranding: true, + parent: { + select: { + hideBranding: true, + }, + }, }, }, }, @@ -71,7 +77,10 @@ async function getUserPageProps(context: GetServerSidePropsContext) { if (hashedLink.eventType.team) { name = hashedLink.eventType.team.slug || ""; - hideBranding = hashedLink.eventType.team.hideBranding; + hideBranding = shouldHideBrandingForTeamEvent({ + eventTypeId: hashedLink.eventTypeId, + team: hashedLink.eventType.team, + }); } else { if (!username) { return notFound; @@ -101,7 +110,10 @@ async function getUserPageProps(context: GetServerSidePropsContext) { return notFound; } - hideBranding = user.hideBranding; + hideBranding = shouldHideBrandingForUserEvent({ + eventTypeId: hashedLink.eventTypeId, + owner: user, + }); } let booking: GetBookingType | null = null; diff --git a/apps/web/lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps.ts b/apps/web/lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps.ts index 5b85b61d87..a7d8c256aa 100644 --- a/apps/web/lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps.ts +++ b/apps/web/lib/org/[orgSlug]/instant-meeting/team/[slug]/[type]/getServerSideProps.ts @@ -5,6 +5,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking"; import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { shouldHideBrandingForTeamEvent } from "@calcom/lib/hideBranding"; import { EventRepository } from "@calcom/lib/server/repository/event"; import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; @@ -28,6 +29,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => select: { id: true, hideBranding: true, + parent: { + select: { + hideBranding: true, + }, + }, }, }); @@ -75,7 +81,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => user: teamSlug, teamId: team.id, slug: meetingSlug, - isBrandingHidden: team?.hideBranding, + isBrandingHidden: shouldHideBrandingForTeamEvent({ + eventTypeId: eventData.id, + team, + }), themeBasis: null, }, }; diff --git a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx index 08e09281cd..5a7a37ace9 100644 --- a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx @@ -8,6 +8,7 @@ import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/org import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings"; import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { shouldHideBrandingForTeamEvent } from "@calcom/lib/hideBranding"; import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; import type { User } from "@calcom/prisma/client"; @@ -156,7 +157,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => user: teamSlug, teamId: team.id, slug: meetingSlug, - isBrandingHidden: team?.hideBranding, + isBrandingHidden: shouldHideBrandingForTeamEvent({ + eventTypeId: eventData.id, + team, + }), isInstantMeeting: eventData && queryIsInstantMeeting ? true : false, themeBasis: null, orgBannerUrl: team.parent?.bannerUrl ?? "", @@ -192,6 +196,7 @@ const getTeamWithEventsData = async ( name: true, bannerUrl: true, logoUrl: true, + hideBranding: true, organizationSettings: { select: { allowSEOIndexing: true, diff --git a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx index 4e399dba9d..9fee836322 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx @@ -6,6 +6,7 @@ import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; +import { shouldHideBrandingForEvent } from "@calcom/lib/hideBranding"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat"; @@ -229,7 +230,12 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { props: { orgSlug: currentOrgDomain, themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username, - hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding, + hideBranding: await shouldHideBrandingForEvent({ + eventTypeId: eventType.id, + team: eventType.team, + owner: eventType.users[0] ?? null, + orgSlug: currentOrgDomain, + }), profile, eventType, recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId), diff --git a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts index 86c2c99cb1..12400d547d 100644 --- a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts @@ -8,6 +8,7 @@ import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/featu import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent"; import { getUsernameList } from "@calcom/lib/defaultEvents"; +import { shouldHideBrandingForUserEvent } from "@calcom/lib/hideBranding"; import { EventRepository } from "@calcom/lib/server/repository/event"; import { UserRepository } from "@calcom/lib/server/repository/user"; import slugify from "@calcom/lib/slugify"; @@ -270,7 +271,10 @@ async function getUserPageProps(context: GetServerSidePropsContext) { eventData: eventData, user: username, slug, - isBrandingHidden: user?.hideBranding, + isBrandingHidden: shouldHideBrandingForUserEvent({ + eventTypeId: eventData.id, + owner: user, + }), isSEOIndexable: allowSEOIndexing, themeBasis: username, bookingUid: bookingUid ? `${bookingUid}` : null, diff --git a/packages/features/ee/payments/pages/payment.tsx b/packages/features/ee/payments/pages/payment.tsx index c18b7e0c86..fd3abdbb14 100644 --- a/packages/features/ee/payments/pages/payment.tsx +++ b/packages/features/ee/payments/pages/payment.tsx @@ -1,7 +1,9 @@ import type { GetServerSidePropsContext } from "next"; import { z } from "zod"; +import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment"; +import { shouldHideBrandingForEvent } from "@calcom/lib/hideBranding"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; import { paymentDataSelect } from "@calcom/prisma/selects/payment"; @@ -16,6 +18,8 @@ const querySchema = z.object({ export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { uid } = querySchema.parse(context.query); + const { currentOrgDomain } = orgDomainConfig(context.req); + const rawPayment = await prisma.payment.findFirst({ where: { uid, @@ -51,7 +55,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const profile = { name: eventType.team?.name || user?.name || null, theme: (!eventType.team?.name && user?.theme) || null, - hideBranding: eventType.team?.hideBranding || user?.hideBranding || null, + hideBranding: await shouldHideBrandingForEvent({ + eventTypeId: eventType.id, + team: eventType.team, + owner: eventType.users[0] ?? null, + orgSlug: currentOrgDomain, + }), }; if ( diff --git a/packages/lib/hideBranding.ts b/packages/lib/hideBranding.ts new file mode 100644 index 0000000000..17cf1409d5 --- /dev/null +++ b/packages/lib/hideBranding.ts @@ -0,0 +1,143 @@ +import logger from "./logger"; +import { ProfileRepository } from "./server/repository/profile"; + +const log = logger.getSubLogger({ name: "hideBranding" }); +type Team = { + hideBranding: boolean | null; + parent: { + hideBranding: boolean | null; + } | null; +}; + +type Profile = { + organization: { + hideBranding: boolean | null; + } | null; +}; + +type UserWithoutProfile = { + id: number; + hideBranding: boolean | null; +}; + +type UserWithProfile = UserWithoutProfile & { + profile: Profile | null; +}; + +type OrgSlug = string | null; + +/** + * Determines if branding should be hidden by checking entity and organization settings + */ +function resolveHideBranding(options: { + entityHideBranding: boolean | null; + organizationHideBranding: boolean | null; +}): boolean { + // If the organization has branding hidden, we should hide branding for the entity regardless of its own setting + if (options.organizationHideBranding) { + return true; + } + + return options.entityHideBranding ?? false; +} + +/** + * Determines if branding should be hidden for an event that could be a team event or user event + */ +export function shouldHideBrandingForEventUsingProfile({ + eventTypeId, + owner, + team, +}: { + owner: UserWithProfile | null; + team: Team | null; + eventTypeId: number; +}) { + let hideBranding; + if (team) { + hideBranding = resolveHideBranding({ + entityHideBranding: team.hideBranding ?? null, + organizationHideBranding: team.parent?.hideBranding ?? null, + }); + } else if (owner) { + hideBranding = resolveHideBranding({ + entityHideBranding: owner.hideBranding ?? null, + organizationHideBranding: owner.profile?.organization?.hideBranding ?? null, + }); + } else { + log.error(`No owner or team found for event: ${eventTypeId}`); + return false; + } + return hideBranding; +} + +/** + * A wrapper over shouldHideBrandingForEventUsingProfile that fetches the profile itself + * Use it when you don't have user's profile + */ +export async function shouldHideBrandingForEvent({ + eventTypeId, + team, + owner, + orgSlug, +}: { + eventTypeId: number; + team: Team | null; + owner: UserWithoutProfile | null; + orgSlug: OrgSlug; +}) { + let ownerProfile = null; + if (team) { + return shouldHideBrandingForTeamEvent({ + team, + eventTypeId, + }); + } else if (owner) { + // Needed only for User events, not for Team events + ownerProfile = orgSlug + ? await ProfileRepository.findByUserIdAndOrgSlug({ + userId: owner.id, + orgSlug, + }) + : null; + + return shouldHideBrandingForUserEvent({ + eventTypeId, + owner: { + ...owner, + profile: ownerProfile, + }, + }); + } else { + log.error(`No owner or team found for event: ${eventTypeId}`); + return false; + } +} + +/** + * A convenience wrapper for shouldHideBrandingForEventUsingProfile to use for Team events + */ +export function shouldHideBrandingForTeamEvent({ eventTypeId, team }: { eventTypeId: number; team: Team }) { + return shouldHideBrandingForEventUsingProfile({ + owner: null, + team, + eventTypeId, + }); +} + +/** + * A convenience wrapper for shouldHideBrandingForEventUsingProfile to use for User events + */ +export function shouldHideBrandingForUserEvent({ + eventTypeId, + owner, +}: { + eventTypeId: number; + owner: UserWithProfile; +}) { + return shouldHideBrandingForEventUsingProfile({ + owner, + team: null, + eventTypeId, + }); +} diff --git a/packages/lib/server/repository/profile.ts b/packages/lib/server/repository/profile.ts index 1df9cae978..37db047b58 100644 --- a/packages/lib/server/repository/profile.ts +++ b/packages/lib/server/repository/profile.ts @@ -50,6 +50,7 @@ const organizationSelect = { logoUrl: true, bannerUrl: true, isPlatform: true, + hideBranding: true, }; const organizationWithSettingsSelect = { ...organizationSelect, @@ -480,6 +481,7 @@ export class ProfileRepository { bannerUrl: true, isPrivate: true, isPlatform: true, + hideBranding: true, organizationSettings: { select: { lockEventTypeCreationForUsers: true, @@ -659,6 +661,18 @@ export class ProfileRepository { return normalizeProfile(profile); } + static async findByUserIdAndOrgSlug({ userId, orgSlug }: { userId: number; orgSlug: string }) { + const profile = await prisma.profile.findFirst({ + where: { userId, organization: { slug: orgSlug } }, + include: { + organization: { + select: organizationSelect, + }, + }, + }); + return profile; + } + /** * Personal profile should come from Profile table only */ diff --git a/packages/prisma/selects/payment.ts b/packages/prisma/selects/payment.ts index fdaadc5c4c..14cfda0cad 100644 --- a/packages/prisma/selects/payment.ts +++ b/packages/prisma/selects/payment.ts @@ -49,6 +49,7 @@ export const paymentDataSelect = Prisma.validator()({ metadata: true, users: { select: { + id: true, name: true, username: true, hideBranding: true, @@ -59,6 +60,11 @@ export const paymentDataSelect = Prisma.validator()({ select: { name: true, hideBranding: true, + parent: { + select: { + hideBranding: true, + }, + }, }, }, price: true,