* feat: implement tiered Intercom chat system replacing Plain - Add TieredIntercomChat component with customer tier detection - Add IntercomContactForm for free users using Intercom API - Add /api/intercom-conversation endpoint for free user support - Update UserDropdown to use tiered chat logic - Replace Plain chat with Intercom in app providers - Remove all Plain chat components and related files - Use useHasPaidPlan hook for customer tier detection Paying customers see Intercom widget, free users see contact form that creates conversations via Intercom API. Co-Authored-By: peer@cal.com <peer@cal.com> * fix: add missing environment variables to turbo.json globalEnv - Add NEXT_PUBLIC_WEBAPP_URL, NEXT_PUBLIC_WEBSITE_URL, NEXT_PUBLIC_STRIPE_PUBLIC_KEY - Add NEXT_PUBLIC_IS_E2E, NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLY - Add NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD, NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID - Resolves turbo/no-undeclared-env-vars ESLint errors blocking commits Co-Authored-By: peer@cal.com <peer@cal.com> * fix: resolve linting errors in platform examples base package - Fix prettier/prettier formatting (quotes and comma) - Add underscore prefix to unused variables - Resolves CI failure in @calcom/base#lint check Co-Authored-By: peer@cal.com <peer@cal.com> * remove plain usage * placement fixes * fix: pop closing immediately issue * stuff * proper error and additional user info * add key to .env.example and remove unused plain route * fix conversation route * refactor intercom dynamic provider * code rabbit fixes * feat: introduce tiered support feature flag * fix: type check * Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
188 lines
6.1 KiB
TypeScript
188 lines
6.1 KiB
TypeScript
// eslint-disable-next-line no-restricted-imports
|
|
import { noop } from "lodash";
|
|
import { useEffect } from "react";
|
|
import type { IntercomBootProps, IntercomProps } from "react-use-intercom";
|
|
import { useIntercom as useIntercomLib } from "react-use-intercom";
|
|
import { z } from "zod";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
|
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
|
import { useHasTeamPlan, useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
|
import { localStorage } from "@calcom/lib/webstorage";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
|
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
export const isInterComEnabled = z.string().min(1).safeParse(process.env.NEXT_PUBLIC_INTERCOM_APP_ID).success;
|
|
|
|
const useIntercomHook = isInterComEnabled
|
|
? useIntercomLib
|
|
: () => {
|
|
return {
|
|
// eslint-disable-next-line
|
|
boot: (_props: IntercomBootProps) => {},
|
|
show: noop,
|
|
shutdown: noop,
|
|
// eslint-disable-next-line
|
|
update: (_props: Partial<IntercomProps>) => {},
|
|
};
|
|
};
|
|
|
|
export const useIntercom = () => {
|
|
const hookData = useIntercomHook();
|
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
|
const { data } = trpc.viewer.me.get.useQuery();
|
|
const { data: statsData } = trpc.viewer.me.myStats.useQuery(undefined, {
|
|
trpc: {
|
|
context: {
|
|
skipBatch: true,
|
|
},
|
|
},
|
|
});
|
|
const { hasPaidPlan } = useHasPaidPlan();
|
|
const { hasTeamPlan } = useHasTeamPlan();
|
|
|
|
const boot = async () => {
|
|
if (!data || !statsData) return;
|
|
let userHash;
|
|
const req = await fetch(`/api/support/hash`);
|
|
const res = await req.json();
|
|
if (res?.hash) {
|
|
userHash = res.hash;
|
|
}
|
|
|
|
hookData.boot({
|
|
...(data && data?.name && { name: data.name }),
|
|
...(data && data?.email && { email: data.email }),
|
|
...(data && data?.id && { userId: data.id }),
|
|
createdAt: String(dayjs(data?.createdDate).unix()),
|
|
...(userHash && { userHash }),
|
|
hideDefaultLauncher: isMobile,
|
|
customAttributes: {
|
|
//keys should be snake cased
|
|
user_name: data?.username,
|
|
link: `${WEBSITE_URL}/${data?.username}`,
|
|
admin_link: `${WEBAPP_URL}/settings/admin/users/${data?.id}/edit`,
|
|
impersonate_user: `${WEBAPP_URL}/settings/admin/impersonation?username=${
|
|
data?.email ?? data?.username
|
|
}`,
|
|
identity_provider: data?.identityProvider,
|
|
timezone: data?.timeZone,
|
|
locale: data?.locale,
|
|
has_paid_plan: hasPaidPlan,
|
|
has_team_plan: hasTeamPlan,
|
|
metadata: data?.metadata,
|
|
completed_onboarding: data.completedOnboarding,
|
|
is_logged_in: !!data,
|
|
sum_of_bookings: statsData?.sumOfBookings,
|
|
sum_of_calendars: statsData?.sumOfCalendars,
|
|
sum_of_teams: statsData?.sumOfTeams,
|
|
has_orgs_plan: !!data?.organizationId,
|
|
organization: data?.organization?.slug,
|
|
sum_of_event_types: statsData?.sumOfEventTypes,
|
|
sum_of_team_event_types: statsData?.sumOfTeamEventTypes,
|
|
is_premium: data?.isPremium,
|
|
},
|
|
});
|
|
};
|
|
|
|
const open = async () => {
|
|
let userHash;
|
|
|
|
const req = await fetch(`/api/support/hash`);
|
|
const res = await req.json();
|
|
if (res?.hash) {
|
|
userHash = res.hash;
|
|
}
|
|
|
|
hookData.boot({
|
|
...(data && data?.name && { name: data.name }),
|
|
...(data && data?.email && { email: data.email }),
|
|
...(data && data?.id && { userId: data.id }),
|
|
createdAt: String(dayjs(data?.createdDate).unix()),
|
|
...(userHash && { userHash }),
|
|
hideDefaultLauncher: isMobile,
|
|
customAttributes: {
|
|
//keys should be snake cased
|
|
user_name: data?.username,
|
|
link: `${WEBSITE_URL}/${data?.username}`,
|
|
admin_link: `${WEBAPP_URL}/settings/admin/users/${data?.id}/edit`,
|
|
impersonate_user: `${WEBAPP_URL}/settings/admin/impersonation?username=${
|
|
data?.email ?? data?.username
|
|
}`,
|
|
identity_provider: data?.identityProvider,
|
|
timezone: data?.timeZone,
|
|
locale: data?.locale,
|
|
has_paid_plan: hasPaidPlan,
|
|
has_team_plan: hasTeamPlan,
|
|
metadata: data?.metadata,
|
|
completed_onboarding: data?.completedOnboarding,
|
|
is_logged_in: !!data,
|
|
sum_of_bookings: statsData?.sumOfBookings,
|
|
sum_of_calendars: statsData?.sumOfCalendars,
|
|
sum_of_teams: statsData?.sumOfTeams,
|
|
has_orgs_plan: !!data?.organizationId,
|
|
organization: data?.organization?.slug,
|
|
sum_of_event_types: statsData?.sumOfEventTypes,
|
|
sum_of_team_event_types: statsData?.sumOfTeamEventTypes,
|
|
is_premium: data?.isPremium,
|
|
},
|
|
});
|
|
hookData.show();
|
|
};
|
|
return { ...hookData, open, boot };
|
|
};
|
|
|
|
declare global {
|
|
interface Window {
|
|
Support?: {
|
|
open: () => void;
|
|
shouldShowTriggerButton: (showTrigger: boolean) => void;
|
|
};
|
|
}
|
|
}
|
|
|
|
export const useBootIntercom = () => {
|
|
const { hasPaidPlan } = useHasPaidPlan();
|
|
const flagMap = useFlagMap();
|
|
const { boot, open, update } = useIntercom();
|
|
|
|
const { data: user } = trpc.viewer.me.get.useQuery();
|
|
const isTieredSupportEnabled = flagMap["tiered-support-chat"];
|
|
const { data: statsData } = trpc.viewer.me.myStats.useQuery(undefined, {
|
|
trpc: {
|
|
context: {
|
|
skipBatch: true,
|
|
},
|
|
},
|
|
});
|
|
useEffect(() => {
|
|
// not using useMediaQuery as it toggles between true and false
|
|
const showIntercom = localStorage.getItem("showIntercom");
|
|
if (
|
|
!isInterComEnabled ||
|
|
showIntercom === "false" ||
|
|
!user ||
|
|
!statsData ||
|
|
(!hasPaidPlan && isTieredSupportEnabled)
|
|
)
|
|
return;
|
|
|
|
boot();
|
|
if (typeof window !== "undefined" && !window.Support) {
|
|
window.Support = {
|
|
open,
|
|
shouldShowTriggerButton: (showTrigger: boolean) => {
|
|
update({
|
|
hideDefaultLauncher: !showTrigger,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [user, statsData, hasPaidPlan, isTieredSupportEnabled]);
|
|
};
|
|
|
|
export default useIntercom;
|