License server (#2379)

* WIP License server

* WIP

* Moves locations to App Store and Core

* LocationType fixes

* Runs db migrations post-deploy

* WIP

* WIP

* Cleanup

* WIP

* WIP

* Decouples translations from NavTabs

* Adds admin submodule

* Adds admin submodule

* Sync dependencies

* WIP

* WIP

* Updates submodules

* Renames package

* Updates submodules

* Adds scripts for console

* Updates license checker URL

* Updates admin

* Adds staging/prod admin console links

* Update yarn.lock

* Update NavTabs.tsx

* WIP

* Update admin

* WIP

* Adds hint to InputField

* Update admin

* Adds turbo admin dependecies

* Update admin

* Prevents redirection on form submit

* Form warning fixes

* Update admin

* Form fixes

* Update yarn.lock

* Update admin

* Update admin

* Update admin

* Adds withLicenseRequired HOC

* Adds LicenseRequired to EE components

* Admin deploy fix?

* Updates submodules

* Use relative inside lib

* type fixes

* Fixes turbo race condition

* Relocates admin to console

* Relocates admin to console

* Update console

* Update api

* Update turbo.json

* Update ErrorBoundary.tsx

* Update defaultEvents.ts

* Update checkLicense.ts

* Update yarn.lock

* Skip on E2E

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Omar Lópezandkodiakhq[bot] authored and GitHub committed 2022-05-26 11:07:14 -06:00
1 parent 6940c8d6cc
commit 9df4867fca
37 files changed
+502 -285

No files matched your search

Submodule apps/api updated: 14aca7ef2b...76b3f1ba3a.
+11 -13
View File
@@ -1,23 +1,21 @@
import { useSession } from "next-auth/react";
import React from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import NavTabs from "./NavTabs";
const tabs = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
export default function AppsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const { status } = useSession();
const tabs = [
{
name: t("app_store"),
href: "/apps",
},
{
name: t("installed_apps"),
href: "/apps/installed",
},
];
return (
<>
+19 -22
View File
@@ -1,30 +1,27 @@
import React from "react";
import { useLocale } from "@lib/hooks/useLocale";
import NavTabs from "./NavTabs";
export default function BookingsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const tabs = [
{
name: t("upcoming"),
href: "/bookings/upcoming",
},
{
name: t("recurring"),
href: "/bookings/recurring",
},
{
name: t("past"),
href: "/bookings/past",
},
{
name: t("cancelled"),
href: "/bookings/cancelled",
},
];
const tabs = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
export default function BookingsShell({ children }: { children: React.ReactNode }) {
return (
<>
<NavTabs tabs={tabs} linkProps={{ shallow: true }} />
+5 -5
View File
@@ -9,17 +9,17 @@ export default function EmptyScreen({
}: {
Icon: SVGComponent;
headline: string;
description: string;
description: string | React.ReactElement;
}) {
return (
<>
<div className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-white">
<Icon className="inline-block h-10 w-10 bg-white" />
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium">{headline}</h2>
<p className="text-sm leading-6 text-gray-600">{description}</p>
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{headline}</h2>
<p className="text-sm leading-6 text-gray-600 dark:text-gray-300">{description}</p>
</div>
</div>
</>
+4 -1
View File
@@ -4,6 +4,8 @@ import Link, { LinkProps } from "next/link";
import { useRouter } from "next/router";
import { FC, Fragment, MouseEventHandler } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@lib/classNames";
import { SVGComponent } from "@lib/types/SVGComponent";
@@ -22,6 +24,7 @@ export interface NavTabProps {
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
const router = useRouter();
const { t } = useLocale();
return (
<>
<nav
@@ -77,7 +80,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
aria-hidden="true"
/>
)}
<span>{tab.name}</span>
<span>{t(tab.name)}</span>
</a>
</Link>
</Component>
+42 -36
View File
@@ -1,48 +1,54 @@
import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
import React from "react";
import React, { ComponentProps } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import ErrorBoundary from "@lib/ErrorBoundary";
import NavTabs from "./NavTabs";
import Shell from "./Shell";
export default function SettingsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const tabs = [
{
name: t("profile"),
href: "/settings/profile",
icon: UserIcon,
},
{
name: t("security"),
href: "/settings/security",
icon: KeyIcon,
},
{
name: t("teams"),
href: "/settings/teams",
icon: UserGroupIcon,
},
{
name: t("billing"),
href: "/settings/billing",
icon: CreditCardIcon,
},
{
name: t("admin"),
href: "/settings/admin",
icon: LockClosedIcon,
adminRequired: true,
},
];
const tabs = [
{
name: "profile",
href: "/settings/profile",
icon: UserIcon,
},
{
name: "security",
href: "/settings/security",
icon: KeyIcon,
},
{
name: "teams",
href: "/settings/teams",
icon: UserGroupIcon,
},
{
name: "billing",
href: "/settings/billing",
icon: CreditCardIcon,
},
{
name: "admin",
href: "/settings/admin",
icon: LockClosedIcon,
adminRequired: true,
},
];
export default function SettingsShell({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return (
<>
<Shell {...rest}>
<div className="sm:mx-auto">
<NavTabs tabs={tabs} />
</div>
<main className="max-w-4xl">{children}</main>
</>
<main className="max-w-4xl">
<>
<ErrorBoundary>{children}</ErrorBoundary>
</>
</main>
</Shell>
);
}
+2 -1
View File
@@ -32,6 +32,7 @@ import LicenseBanner from "@ee/components/LicenseBanner";
import TrialBanner from "@ee/components/TrialBanner";
import HelpMenuItem from "@ee/components/support/HelpMenuItem";
import ErrorBoundary from "@lib/ErrorBoundary";
import classNames from "@lib/classNames";
import { WEBAPP_URL } from "@lib/config/constants";
import { shouldShowOnboarding } from "@lib/getting-started";
@@ -349,7 +350,7 @@ const Layout = ({
"px-4 sm:px-6 md:px-8",
props.flexChildrenContainer && "flex flex-1 flex-col"
)}>
{!props.isLoading ? props.children : props.customLoader}
<ErrorBoundary>{!props.isLoading ? props.children : props.customLoader}</ErrorBoundary>
</div>
{/* show bottom navigation for md and smaller (tablet and phones) */}
{status === "authenticated" && (
+1 -1
View File
@@ -18,7 +18,7 @@ export default function ModalContainer(props: Props) {
<DialogContent>
<div
className={classNames(
"inline-block transform bg-white text-left align-bottom transition-all sm:align-middle",
"inline-block w-full transform bg-white text-left align-bottom transition-all sm:align-middle",
{
"sm:w-full sm:max-w-lg ": !props.wide,
"sm:w-4xl sm:max-w-4xl": props.wide,
@@ -0,0 +1,57 @@
import { ExclamationIcon } from "@heroicons/react/solid";
import { useSession } from "next-auth/react";
import React, { AriaRole, ComponentType, FC, Fragment } from "react";
import { CONSOLE_URL } from "@calcom/lib/constants";
import EmptyScreen from "@components/EmptyScreen";
type LicenseRequiredProps = {
as?: keyof JSX.IntrinsicElements | "";
className?: string;
role?: AriaRole | undefined;
children: React.ReactNode;
};
/**
* This component will only render it's children if the installation has a valid
* license.
*/
const LicenseRequired: FC<LicenseRequiredProps> = ({ children, as = "", ...rest }) => {
const session = useSession();
const Component = as || Fragment;
return (
<Component {...rest}>
{session.data?.hasValidLicense ? (
children
) : (
<EmptyScreen
Icon={ExclamationIcon}
headline="This is an enterprise feature"
description={
<>
To enable this feature, get a deployment key at{" "}
<a href={CONSOLE_URL} target="_blank" rel="noopener noreferrer" className="underline">
Cal.com console
</a>
.
</>
}
/>
)}
</Component>
);
};
export function withLicenseRequired<T>(Component: ComponentType<T>) {
// eslint-disable-next-line react/display-name
return (hocProps: T) => {
return (
<LicenseRequired>
<Component {...(hocProps as T)} />;
</LicenseRequired>
);
};
}
export default LicenseRequired;
@@ -15,11 +15,12 @@ import { trpc } from "@lib/trpc";
import { DatePicker } from "@components/ui/form/DatePicker";
import { TApiKeys } from "./ApiKeyListItem";
import LicenseRequired from "../LicenseRequired";
import type { TApiKeys } from "./ApiKeyListItem";
export default function ApiKeyDialogForm(props: {
title: string;
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean };
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean };
handleClose: () => void;
}) {
const { t } = useLocale();
@@ -49,7 +50,7 @@ export default function ApiKeyDialogForm(props: {
const watchNeverExpires = form.watch("neverExpires");
return (
<>
<LicenseRequired>
{successfulNewApiKeyModal ? (
<>
<div className="mb-10">
@@ -92,12 +93,12 @@ export default function ApiKeyDialogForm(props: {
</DialogFooter>
</>
) : (
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean }>
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean }>
form={form}
handleSubmit={async (event) => {
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
setApiKey(apiKey);
setApiKeyDetails({ ...event });
setApiKeyDetails({ ...event, neverExpires: !!event.neverExpires });
await utils.invalidateQueries(["viewer.apiKeys.list"]);
setSuccessfulNewApiKeyModal(true);
}}
@@ -146,6 +147,6 @@ export default function ApiKeyDialogForm(props: {
</DialogFooter>
</Form>
)}
</>
</LicenseRequired>
);
}
@@ -12,66 +12,76 @@ import { trpc } from "@lib/trpc";
import { List } from "@components/List";
export default function ApiKeyListContainer() {
import LicenseRequired from "../LicenseRequired";
function ApiKeyListContainer() {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.apiKeys.list"]);
const [newApiKeyModal, setNewApiKeyModal] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires: boolean }) | null>(null);
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | null>(null);
return (
<QueryCell
query={query}
success={({ data }) => (
<>
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
<div className="mt-9">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
</div>
<div className="mb-9 sm:self-center">
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
{t("generate_new_api_key")}
</Button>
</div>
</div>
{data.length > 0 && (
<List className="pb-6">
{data.map((item: any) => (
<ApiKeyListItem
key={item.id}
apiKey={item}
onEditApiKey={() => {
setApiKeyToEdit(item);
setEditModalOpen(true);
}}
/>
))}
</List>
)}
{/* New api key dialog */}
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
<DialogContent>
<ApiKeyDialogForm title={t("create_api_key")} handleClose={() => setNewApiKeyModal(false)} />
</DialogContent>
</Dialog>
{/* Edit api key dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent>
{apiKeyToEdit && (
<ApiKeyDialogForm
title={t("edit_api_key")}
key={apiKeyToEdit.id}
handleClose={() => setEditModalOpen(false)}
defaultValues={apiKeyToEdit}
/>
<>
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
<div className="mt-9">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
</div>
<div className="mb-9 sm:self-center">
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
{t("generate_new_api_key")}
</Button>
</div>
</div>
<LicenseRequired>
<QueryCell
query={query}
success={({ data }) => (
<>
{data.length > 0 && (
<List className="pb-6">
{data.map((item) => (
<ApiKeyListItem
key={item.id}
apiKey={item}
onEditApiKey={() => {
setApiKeyToEdit(item);
setEditModalOpen(true);
}}
/>
))}
</List>
)}
</DialogContent>
</Dialog>
</>
)}
/>
{/* New api key dialog */}
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
<DialogContent>
<ApiKeyDialogForm
title={t("create_api_key")}
handleClose={() => setNewApiKeyModal(false)}
/>
</DialogContent>
</Dialog>
{/* Edit api key dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent>
{apiKeyToEdit && (
<ApiKeyDialogForm
title={t("edit_api_key")}
key={apiKeyToEdit.id}
handleClose={() => setEditModalOpen(false)}
defaultValues={apiKeyToEdit}
/>
)}
</DialogContent>
</Dialog>
</>
)}
/>
</LicenseRequired>
</>
);
}
export default ApiKeyListContainer;
@@ -1,18 +1,20 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { TextArea } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Badge from "@components/ui/Badge";
import LicenseRequired from "../LicenseRequired";
export default function SAMLConfiguration({
teamsView,
teamId,
@@ -92,7 +94,7 @@ export default function SAMLConfiguration({
return (
<>
{isSAMLLoginEnabled ? (
<>
<LicenseRequired>
<hr className="mt-8" />
<div className="mt-6">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
@@ -157,7 +159,7 @@ export default function SAMLConfiguration({
</div>
<hr className="mt-4" />
</form>
</>
</LicenseRequired>
) : null}
</>
);
@@ -11,6 +11,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core";
import getStripe from "@calcom/stripe/client";
import LicenseRequired from "@ee/components/LicenseRequired";
import PaymentComponent from "@ee/components/stripe/Payment";
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
@@ -70,7 +71,8 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<div
<LicenseRequired
as="div"
className={classNames(
"main inline-block transform overflow-hidden rounded-lg border border-neutral-200 bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle",
isEmbed ? "" : "sm:my-8"
@@ -151,7 +153,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
<a href="https://cal.com/signup">{t("create_booking_link_with_calcom")}</a>
</div>
)}
</div>
</LicenseRequired>
</div>
</div>
</div>
@@ -3,8 +3,9 @@ import utc from "dayjs/plugin/utc";
import React, { useState, useEffect } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants";
import LicenseRequired from "@ee/components/LicenseRequired";
import { trpc, inferQueryOutput } from "@lib/trpc";
import { inferQueryOutput, trpc } from "@lib/trpc";
import Avatar from "@components/ui/Avatar";
import { DatePicker } from "@components/ui/form/DatePicker";
@@ -33,62 +34,66 @@ export default function TeamAvailabilityModal(props: Props) {
}, [utils, selectedTimeZone, selectedDate]);
return (
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
<div className="min-w-64 w-64 space-y-5 p-5 pr-0">
<div className="flex">
<Avatar
imageSrc={WEBSITE_URL + "/" + props.member?.username + "/avatar.png"}
alt={props.member?.name || ""}
className="h-14 w-14 rounded-full"
/>
<div className="ml-3 inline-block pt-1">
<span className="text-lg font-bold text-neutral-700">{props.member?.name}</span>
<span className="-mt-1 block text-sm text-gray-400">{props.member?.email}</span>
<LicenseRequired>
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
<div className="min-w-64 w-64 space-y-5 p-5 pr-0">
<div className="flex">
<Avatar
imageSrc={WEBSITE_URL + "/" + props.member?.username + "/avatar.png"}
alt={props.member?.name || ""}
className="h-14 w-14 rounded-full"
/>
<div className="ml-3 inline-block pt-1">
<span className="text-lg font-bold text-neutral-700">{props.member?.name}</span>
<span className="-mt-1 block text-sm text-gray-400">{props.member?.email}</span>
</div>
</div>
<div className="flex flex-col">
<span className="font-bold text-gray-600">Date</span>
<DatePicker
date={selectedDate.toDate()}
onDatesChange={(newDate) => {
setSelectedDate(dayjs(newDate));
}}
/>
</div>
<div>
<span className="font-bold text-gray-600">Timezone</span>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
classNamePrefix="react-select"
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
/>
</div>
<div>
<span className="font-bold text-gray-600">Slot Length</span>
<Select
options={[
{ value: 15, label: "15 minutes" },
{ value: 30, label: "30 minutes" },
{ value: 60, label: "60 minutes" },
]}
isSearchable={false}
classNamePrefix="react-select"
className="react-select-container focus:ring-primary-500 focus:border-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
value={{ value: frequency, label: `${frequency} minutes` }}
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
/>
</div>
</div>
<div className="flex flex-col">
<span className="font-bold text-gray-600">Date</span>
<DatePicker
date={selectedDate.toDate()}
onDatesChange={(newDate) => {
setSelectedDate(dayjs(newDate));
}}
{props.team && props.member && (
<TeamAvailabilityTimes
className="overflow-scroll"
teamId={props.team.id}
memberId={props.member.id}
frequency={frequency}
selectedDate={selectedDate}
selectedTimeZone={selectedTimeZone}
/>
</div>
<div>
<span className="font-bold text-gray-600">Timezone</span>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
className="mt-1 block w-full rounded-sm border border-gray-300 shadow-sm sm:text-sm"
/>
</div>
<div>
<span className="font-bold text-gray-600">Slot Length</span>
<Select
options={[
{ value: 15, label: "15 minutes" },
{ value: 30, label: "30 minutes" },
{ value: 60, label: "60 minutes" },
]}
isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
value={{ value: frequency, label: `${frequency} minutes` }}
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
/>
</div>
)}
</div>
{props.team && props.member && (
<TeamAvailabilityTimes
className="overflow-auto"
teamId={props.team.id}
memberId={props.member.id}
frequency={frequency}
selectedDate={selectedDate}
selectedTimeZone={selectedTimeZone}
/>
)}
</div>
</LicenseRequired>
);
}
@@ -11,6 +11,7 @@ import { Button } from "@calcom/ui/Button";
import { useContracts } from "../../../contexts/contractsContext";
import genericAbi from "../../../web3/abis/abiWithGetBalance.json";
import verifyAccount, { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount";
import { withLicenseRequired } from "../LicenseRequired";
interface Window {
ethereum: AbstractProvider & { selectedAddress: string };
@@ -150,4 +151,4 @@ const CryptoSection = (props: CryptoSectionProps) => {
);
};
export default CryptoSection;
export default withLicenseRequired(CryptoSection);
@@ -2,6 +2,7 @@ import { useRouter } from "next/router";
import { useMemo, useState } from "react";
import { Alert } from "@calcom/ui/Alert";
import LicenseRequired from "@ee/components/LicenseRequired";
import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
@@ -49,17 +50,19 @@ export function TeamAvailabilityPage() {
/>
)
}>
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
{isLoading && <Loader />}
{isFreeUser ? (
<Alert
className="-mt-24 border"
severity="warning"
title="This is a pro feature. Upgrade to pro to see your team's availability."
/>
) : (
TeamAvailability
)}
<LicenseRequired>
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
{isLoading && <Loader />}
{isFreeUser ? (
<Alert
className="-mt-24 border"
severity="warning"
title="This is a pro feature. Upgrade to pro to see your team's availability."
/>
) : (
TeamAvailability
)}
</LicenseRequired>
</Shell>
);
}
+35
View File
@@ -0,0 +1,35 @@
import React, { ErrorInfo } from "react";
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ error: Error | null; errorInfo: ErrorInfo | null }
> {
constructor(props: { children: React.ReactNode } | Readonly<{ children: React.ReactNode }>) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch?(error: Error, errorInfo: ErrorInfo) {
// Catch errors in any components below and re-render with error message
this.setState({ error, errorInfo });
// You can also log error messages to an error reporting service here
}
render() {
if (this.state.errorInfo) {
// Error path
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: "pre-wrap" }}>
{this.state.error && this.state.error.toString()}
</details>
</div>
);
}
// Normally, just render children
return this.props.children;
}
}
export default ErrorBoundary;
+2 -1
View File
@@ -16,7 +16,8 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps & { children: React.Rea
));
// Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = NextAppProps & {
export type AppProps = Omit<NextAppProps, "Component"> & {
Component: NextAppProps["Component"] & { requiresLicense?: boolean };
/** Will be defined only is there was an error */
err?: Error;
};
+2
View File
@@ -76,6 +76,7 @@
"jimp": "^0.16.1",
"libphonenumber-js": "^1.9.53",
"lodash": "^4.17.21",
"memory-cache": "^0.2.0",
"micro": "^9.3.4",
"mime-types": "^2.1.35",
"next": "^12.1.6",
@@ -125,6 +126,7 @@
"@types/glidejs__glide": "^3.4.2",
"@types/jest": "^27.5.1",
"@types/lodash": "^4.14.182",
"@types/memory-cache": "^0.2.2",
"@types/micro": "7.3.6",
"@types/mime-types": "^2.1.1",
"@types/module-alias": "^2.0.1",
+8 -1
View File
@@ -3,6 +3,7 @@ import Head from "next/head";
import superjson from "superjson";
import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@ee/components/LicenseRequired";
import AppProviders, { AppProps } from "@lib/app-providers";
import { seoConfig } from "@lib/config/next-seo.config";
@@ -37,7 +38,13 @@ function MyApp(props: AppProps) {
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
<Component {...pageProps} err={err} />
{Component.requiresLicense ? (
<LicenseRequired>
<Component {...pageProps} err={err} />
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
)}
</AppProviders>
</ContractsProvider>
);
@@ -10,6 +10,7 @@ import nodemailer, { TransportOptions } from "nodemailer";
import { authenticator } from "otplib";
import path from "path";
import checkLicense from "@calcom/ee/server/checkLicense";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
@@ -276,8 +277,10 @@ export default NextAuth({
return token;
},
async session({ session, token }) {
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
const calendsoSession: Session = {
...session,
hasValidLicense,
user: {
...session.user,
id: token.id as number,
+5 -6
View File
@@ -1,14 +1,13 @@
import { ExternalLinkIcon } from "@heroicons/react/solid";
import { ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { useIntercom } from "@ee/lib/intercom/useIntercom";
import { useLocale } from "@lib/hooks/useLocale";
import useMeQuery from "@lib/hooks/useMeQuery";
import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
type CardProps = { title: string; description: string; className?: string; children: ReactNode };
const Card = ({ title, description, className = "", children }: CardProps): JSX.Element => (
@@ -30,8 +29,8 @@ export default function Billing() {
const { boot, show } = useIntercom();
return (
<Shell heading={t("billing")} subtitle={t("manage_your_billing_info")}>
<SettingsShell>
<SettingsShell heading={t("billing")} subtitle={t("manage_your_billing_info")}>
<>
<div className="py-6 lg:col-span-9 lg:pb-8">
{data?.plan && ["FREE", "TRIAL"].includes(data.plan) && (
<Card
@@ -72,7 +71,7 @@ export default function Billing() {
</div>
</div>
</div>
</SettingsShell>
</Shell>
</>
</SettingsShell>
);
}
+4 -7
View File
@@ -5,6 +5,7 @@ import { signOut } from "next-auth/react";
import { useRouter } from "next/router";
import { ComponentProps, FormEvent, RefObject, useEffect, useMemo, useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
@@ -15,7 +16,6 @@ import { withQuery } from "@lib/QueryCell";
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { nameOfDay } from "@lib/core/i18n/weekday";
import { useLocale } from "@lib/hooks/useLocale";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import prisma from "@lib/prisma";
import { trpc } from "@lib/trpc";
@@ -23,7 +23,6 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import ImageUploader from "@components/ImageUploader";
import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
import Badge from "@components/ui/Badge";
@@ -488,11 +487,9 @@ export default function Settings(props: Props) {
const { t } = useLocale();
return (
<Shell heading={t("profile")} subtitle={t("edit_profile_info_description")}>
<SettingsShell>
<WithQuery success={({ data }) => <SettingsView {...props} localeProp={data.locale} />} />
</SettingsShell>
</Shell>
<SettingsShell heading={t("profile")} subtitle={t("edit_profile_info_description")}>
<WithQuery success={({ data }) => <SettingsView {...props} localeProp={data.locale} />} />
</SettingsShell>
);
}
+4 -5
View File
@@ -9,7 +9,6 @@ import { identityProviderNameMap } from "@lib/auth";
import { trpc } from "@lib/trpc";
import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
import ChangePasswordSection from "@components/security/ChangePasswordSection";
import DisableUserImpersonation from "@components/security/DisableUserImpersonation";
import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection";
@@ -18,8 +17,8 @@ export default function Security() {
const user = trpc.useQuery(["viewer.me"]).data;
const { t } = useLocale();
return (
<Shell heading={t("security")} subtitle={t("manage_account_security")}>
<SettingsShell>
<SettingsShell heading={t("security")} subtitle={t("manage_account_security")}>
<>
{user && user.identityProvider !== IdentityProvider.CAL ? (
<>
<div className="mt-6">
@@ -45,7 +44,7 @@ export default function Security() {
)}
<SAMLConfiguration teamsView={false} teamId={null} />
</SettingsShell>
</Shell>
</>
</SettingsShell>
);
}
+10 -7
View File
@@ -4,21 +4,20 @@ import { useSession } from "next-auth/react";
import { Trans } from "next-i18next";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import useMeQuery from "@lib/hooks/useMeQuery";
import { trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen";
import Loader from "@components/Loader";
import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
import TeamCreateModal from "@components/team/TeamCreateModal";
import TeamList from "@components/team/TeamList";
export default function Teams() {
function Teams() {
const { t } = useLocale();
const { status } = useSession();
const loading = status === "loading";
@@ -40,8 +39,8 @@ export default function Teams() {
const isFreePlan = me.data?.plan === "FREE";
return (
<Shell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
<SettingsShell>
<SettingsShell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
<>
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
{isFreePlan && (
<Alert
@@ -87,7 +86,11 @@ export default function Teams() {
/>
)}
{teams.length > 0 && <TeamList teams={teams}></TeamList>}
</SettingsShell>
</Shell>
</>
</SettingsShell>
);
}
Teams.requiresLicense = false;
export default Teams;