* patch applied

* patch applied

* We shouldn't pollute global css

* Build fixes

* Updates typings

* WIP extracting zoom to package

* Revert "Upgrades next to 12.1 (#1895)" (#1903)

This reverts commit ede0e98e1f.

* Tweak/gitignore prisma zod (#1905)

* Extracts ignored createEventTypeBaseInput

* Adds postinstall script

* Revert "Tweak/gitignore prisma zod (#1905)" (#1906)

This reverts commit 15bfeb30d7.

* Eslint fixes (#1898)

* Eslint fixes

* Docs build fixes

* Upgrade to next 12.1 (#1904)

* Upgrades next to 12.1

* Fixes build

* Updaters e2e test pipelines

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Fix URL by removing slash and backslash (#1733)

* Fix URl by removing slash and backslash

* Implement slugify

* Add data type

* Fixing folder structure

* Solve zod-utils conflict

* Build fixes (#1929)

* Build fixes

* Fixes type error

* WIP

* Conflict fixes

* Removes unused file

* TODO

* WIP

* Type fixes

* Linting

* WIP

* Moved App definition to types

* WIP

* WIP

* WIP

* WIP WIP

* Renamed zoomvideo app

* Import fix

* Daily.co app (#2022)

* Daily.co app

* Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts

Co-authored-by: Omar López <zomars@me.com>

* Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts

Co-authored-by: Omar López <zomars@me.com>

* Missing deps for newly added contants to lib

Co-authored-by: Omar López <zomars@me.com>

* WIP

* WIP

* WIP

* Daily fixes

* Updated type info

* Slack Oauth integration - api route ideas

* Adds getLocationOptions

* Type fixes

* Adds location option for daily video

* Revert "Slack Oauth integration - api route ideas"

This reverts commit 35ffa78e92.

* Slack Oauth + verify sig

* Revert "Slack Oauth + verify sig"

This reverts commit ee95795e0f.

* Huddle01 migration to app store (#2038)

* Jitsi Video App migration

* Removing uneeded dependencies

* Missed unused reference

* Missing dependency

`@calcom/lib` is needed in the `locationOption.ts` file

* Huddle01 migration to app store

* Jitsi Video App migration (#2027)

* Jitsi Video App migration

* Removing uneeded dependencies

* Missed unused reference

* Missing dependency

`@calcom/lib` is needed in the `locationOption.ts` file

Co-authored-by: Omar López <zomars@me.com>

* Monorepo/app store MS Teams Integration (#2080)

* Create teamsvideo package

* Remove zoom specific refrences

* Add teams video files

* Rename to office365_video

* Add call back to add crednetial type office365_teams

* Rename to office_video to match type

* Add MS Teams as a location option

* Rename files

* Add teams reponse interface and create meeting

* Comment out Daily imports

* Add check for Teams integration

* Add token checking functions

* Change template to create event rather than meeting

* Add comment to test between create link and event

* Add teams URL to booking

* Ask for just onlineMeeting permission

* Add MS Teams logo

* Add message to have an enterprise account

* Remove comments

* Comment back hasDailyIntegration

* Comment back daily credentials

* Update link to MS Graph section of README

* Move API calls to package

Co-authored-by: Omar López <zomars@me.com>

* Re-adds missing module for transpiling

* Adds email as required field for app store metadata

* WIP: migrates tandem to app store

* Cleanup

* Migrates tandem api routes to app store

* Fixes tandem api handlers

* Big WIP WIP

* Build fixes

* WIP

* Fixes annoying circular dependency bug

I've spent a whole day on this....

* Location option cleanup

* Type fixes

* Update EventManager.ts

* Update CalendarManager.ts

* Moves CalendarService back to lib

* Moves apple calendar to App Store

* Cleanup

* More cleanup

* Migrates apple calendar

* Returns all connected calendars credentials

* No tsx needed in calcom/lib

* Update auth.ts

* Reordering

* Update i18n.utils.ts

* WIP: Google Meet

* Type fixes

* Type fixes

* Cleanup

* Update LinkIconButton.tsx

* Update TrialBanner.tsx

* Cleanup

* Cleanup

* Type fixes

* Update _appRegistry.ts

* Update fonts.css

* Update CalEventParser.ts

* Delete yarn.lock.rej

* Update eslint-preset.js

* Delete zoom.tsx

* Type fixes

* Migrates caldav to app store

* Cleanup

* Type fixes

* Adds caldav to app store

* Test fixes

* Updates integration tests

* Moar test fixes

* Redirection fixes

* Redirection fixes

* Update timeFormat.ts

* Update booking-pages.test.ts

* Connect button fixes

* Fix empty item

* Cal fixes andrea (#2234)

* Fixes #2178

* Fixes #2178

* Update apps/web/components/availability/Schedule.tsx

* Update apps/web/components/availability/Schedule.tsx

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>

* added meta viewport to disable zoom on input focus on mobile (#2238)

* Update lint.yml (#2211)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Fix prisma client bundle makes app slow (#2237)

Co-authored-by: Omar López <zomars@me.com>

* Slider fixes

* Removed unused code

* Full Shell when unauthed

* App sidebar responsive fixes

* Adds dynamic install button

* Fix for duplicate connected calendars

* Various fixes

* Display notification on app delete

* Reuse connect button

* Adds CalDav button

* Deprecates ConnectIntegration

* Simplify install button

* Adds Google Calendar connect button

* Adds Office 365 Install button

* Migrates Stripe to App Store

* Zoom Install Button (#2244)

* Fix minor css, app image load from static path

* Fix app logos remote img src (#2252)

* Adds missing exports

* Cleanup

* Disables install button for globally enabled apps

* Update EventManager.ts

* Stripe fixes

* Disables example app

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Juan Esteban Nieto Cifuentes <89233604+Jenietoc@users.noreply.github.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Sean Brydon <seanbrydon.me@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: andreaestefania12 <andreaestefania12@hotmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Demian Caldelas <denik.works@protonmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
This commit is contained in:
Omar López 2022-03-23 15:00:30 -07:00 committed by GitHub
parent caeb2412de
commit f536d1040c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
242 changed files with 4322 additions and 2260 deletions

231
apps/web/components/App.tsx Normal file
View File

@ -0,0 +1,231 @@
import {
BookOpenIcon,
DocumentTextIcon,
ExternalLinkIcon,
FlagIcon,
MailIcon,
ShieldCheckIcon,
} from "@heroicons/react/outline";
import { ChevronLeftIcon } from "@heroicons/react/solid";
import Link from "next/link";
import React from "react";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { App as AppType } from "@calcom/types/App";
import { Button } from "@calcom/ui";
//import NavTabs from "@components/NavTabs";
import Shell from "@components/Shell";
import Badge from "@components/ui/Badge";
export default function App({
name,
type,
logo,
body,
categories,
author,
price = 0,
commission,
isGlobal = false,
feeType,
docs,
website,
email,
tos,
privacy,
}: {
name: string;
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
body: React.ReactNode;
categories: string[];
author: string;
pro?: boolean;
price?: number;
commission?: number;
feeType?: AppType["feeType"];
docs?: string;
website?: string;
email: string; // required
tos?: string;
privacy?: string;
}) {
const { t } = useLocale();
/*const tabs = [
{
name: t("description"),
href: "?description",
},
{
name: t("features"),
href: "?features",
},
{
name: t("permissions"),
href: "?permissions",
},
];*/
const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price);
return (
<>
<Shell large>
<div className="-mx-8">
<div className="bg-gray-50 px-10">
<Link href="/apps">
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
<ChevronLeftIcon className="h-5 w-5" /> {t("browse_apps")}
</a>
</Link>
<div className="flex items-center justify-between py-8">
<div className="flex">
<img className="h-16 w-16" src={logo} alt="" />
<header className="px-4 py-2">
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
<h2 className="text-sm text-gray-500">
<span className="capitalize">{categories[0]}</span> {t("published_by", { author })}
</h2>
</header>
</div>
<div className="text-right">
{isGlobal ? (
<Button color="secondary" disabled title="This app is globally installed">
{t("installed")}
</Button>
) : (
<InstallAppButton
type={type}
render={(buttonProps) => <Button {...buttonProps}>{t("install_app")}</Button>}
/>
)}
{price !== 0 && (
<small className="block text-right">
{feeType === "usage-based"
? commission + "% + " + priceInDollar + "/booking"
: priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</small>
)}
</div>
</div>
{/* reintroduce once we show permissions and features
<NavTabs tabs={tabs} linkProps={{ shallow: true }} /> */}
</div>
<div className="justify-between px-10 py-10 md:flex">
<div className="prose-sm prose">{body}</div>
<div className="md:max-w-80 flex-1 md:ml-8">
<h4 className="font-medium text-gray-900 ">{t("categories")}</h4>
<div className="space-x-2">
{categories.map((category) => (
<Link href={"/apps/categories/" + category} key={category}>
<a>
<Badge variant="success">{category}</Badge>
</a>
</Link>
))}
</div>
<h4 className="mt-8 font-medium text-gray-900 ">{t("pricing")}</h4>
<small>
{price === 0 ? (
"Free"
) : (
<>
{Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && "/" + t("month")}
</>
)}
</small>
<h4 className="mt-8 mb-2 font-medium text-gray-900 ">{t("learn_more")}</h4>
<ul className="prose -ml-1 -mr-1 text-xs leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={docs}>
<BookOpenIcon className="mr-1 -mt-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
)}
{website && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={website}>
<ExternalLinkIcon className="mr-1 -mt-px inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
)}
{email && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={"mailto:" + email}>
<MailIcon className="mr-1 -mt-px inline h-4 w-4" />
{email}
</a>
</li>
)}
{tos && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={tos}>
<DocumentTextIcon className="mr-1 -mt-px inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
)}
{privacy && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={privacy}>
<ShieldCheckIcon className="mr-1 -mt-px inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="my-6" />
<small className="leading-1 block text-gray-500">
Every app published on the Cal.com App Store is open source and thoroughly tested via peer
reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
</small>
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
<FlagIcon className="inline h-3 w-3" /> Report App
</a>
</div>
</div>
</div>
</Shell>
</>
);
}

View File

@ -0,0 +1,30 @@
import { useSession } from "next-auth/react";
import React from "react";
import { useLocale } from "@lib/hooks/useLocale";
import NavTabs from "./NavTabs";
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 (
<>
<div className="mb-12 block lg:hidden">
{status === "authenticated" && <NavTabs tabs={tabs} linkProps={{ shallow: true }} />}
</div>
<main>{children}</main>
</>
);
}

View File

@ -67,7 +67,7 @@ const DestinationCalendarSelector = ({
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
options={options}
isSearchable={false}
className="focus:border-primary-500 focus:ring-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
className="focus:ring-primary-500 focus:border-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
onChange={(option) => {
setSelectedOption(option);
if (!option) {

View File

@ -1,20 +1,20 @@
import { SelectorIcon } from "@heroicons/react/outline";
import {
CalendarIcon,
ArrowLeftIcon,
ClockIcon,
CogIcon,
ExternalLinkIcon,
LinkIcon,
LogoutIcon,
PuzzleIcon,
ViewGridIcon,
MoonIcon,
MapIcon,
ArrowLeftIcon,
} from "@heroicons/react/solid";
import { signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { ReactNode, useEffect, useState } from "react";
import React, { Fragment, ReactNode, useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import Button from "@calcom/ui/Button";
@ -59,6 +59,10 @@ function useRedirectToLoginIfUnauthenticated() {
const router = useRouter();
useEffect(() => {
if (router.pathname.startsWith("/apps")) {
return;
}
if (!loading && !session) {
router.replace({
pathname: "/auth/login",
@ -121,10 +125,11 @@ export function ShellSubHeading(props: {
export default function Shell(props: {
centered?: boolean;
title?: string;
heading: ReactNode;
heading?: ReactNode;
subtitle?: ReactNode;
children: ReactNode;
CTA?: ReactNode;
large?: boolean;
HeadingLeftIcon?: ReactNode;
backPath?: string; // renders back button to specified path
// use when content needs to expand with flex
@ -157,10 +162,22 @@ export default function Shell(props: {
current: router.asPath.startsWith("/availability"),
},
{
name: t("integrations"),
href: "/integrations",
icon: PuzzleIcon,
current: router.asPath.startsWith("/integrations"),
name: t("apps"),
href: "/apps",
icon: ViewGridIcon,
current: router.asPath.startsWith("/apps"),
child: [
{
name: t("app_store"),
href: "/apps",
current: router.asPath === "/apps",
},
{
name: t("installed_apps"),
href: "/apps/installed",
current: router.asPath === "/apps/installed",
},
],
},
{
name: t("settings"),
@ -182,6 +199,7 @@ export default function Shell(props: {
const user = query.data;
const i18n = useViewerI18n();
const { status } = useSession();
if (i18n.status === "loading" || isRedirectingToOnboarding || loading) {
// show spinner whilst i18n is loading to avoid language flicker
@ -206,95 +224,121 @@ export default function Shell(props: {
<Toaster position="bottom-right" />
</div>
<div className="flex h-screen overflow-hidden bg-gray-100" data-testid="dashboard-shell">
<div className="hidden md:flex lg:flex-shrink-0">
<div className="flex w-14 flex-col lg:w-56">
<div className="flex h-0 flex-1 flex-col border-r border-gray-200 bg-white">
<div className="flex flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5">
<Link href="/event-types">
<a className="px-4 md:hidden lg:inline">
<Logo small />
</a>
</Link>
{/* logo icon for tablet */}
<Link href="/event-types">
<a className="md:inline lg:hidden">
<Logo small icon />
</a>
</Link>
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
{navigation.map((item) => (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current
? "bg-neutral-100 text-neutral-900"
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900",
"group flex items-center rounded-sm px-2 py-2 text-sm font-medium"
)}>
<item.icon
className={classNames(
item.current
? "text-neutral-500"
: "text-neutral-400 group-hover:text-neutral-500",
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
)}
aria-hidden="true"
/>
<span className="hidden lg:inline">{item.name}</span>
</a>
</Link>
))}
</nav>
<div
className={classNames("flex h-screen overflow-hidden", props.large ? "bg-white" : "bg-gray-100")}
data-testid="dashboard-shell">
{status === "authenticated" && (
<div className="hidden md:flex lg:flex-shrink-0">
<div className="flex w-14 flex-col lg:w-56">
<div className="flex h-0 flex-1 flex-col border-r border-gray-200 bg-white">
<div className="flex flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5">
<Link href="/event-types">
<a className="px-4 md:hidden lg:inline">
<Logo small />
</a>
</Link>
{/* logo icon for tablet */}
<Link href="/event-types">
<a className="md:inline lg:hidden">
<Logo small icon />
</a>
</Link>
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
{navigation.map((item) => (
<Fragment key={item.name}>
<Link href={item.href}>
<a
className={classNames(
item.current
? "bg-neutral-100 text-neutral-900"
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900",
"group flex items-center rounded-sm px-2 py-2 text-sm font-medium"
)}>
<item.icon
className={classNames(
item.current
? "text-neutral-500"
: "text-neutral-400 group-hover:text-neutral-500",
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
)}
aria-hidden="true"
/>
<span className="hidden lg:inline">{item.name}</span>
</a>
</Link>
{item.child &&
router.asPath.startsWith(item.href) &&
item.child.map((item) => {
return (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current
? "text-neutral-900"
: "text-neutral-500 hover:text-neutral-900",
"group hidden items-center rounded-sm px-2 py-2 pl-10 text-sm font-medium lg:flex"
)}>
<span className="hidden lg:inline">{item.name}</span>
</a>
</Link>
);
})}
</Fragment>
))}
</nav>
</div>
<TrialBanner />
<div className="rounded-sm pb-2 pl-3 pt-2 pr-2 hover:bg-gray-100 lg:mx-2 lg:pl-2">
<span className="hidden lg:inline">
<UserDropdown />
</span>
<span className="hidden md:inline lg:hidden">
<UserDropdown small />
</span>
</div>
<small style={{ fontSize: "0.5rem" }} className="mx-3 mt-1 mb-2 hidden opacity-50 lg:block">
&copy; {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"}
{process.env.NEXT_PUBLIC_APP_URL === "https://cal.com" ? "h" : "sh"}
<span className="lowercase">-{user && user.plan}</span>
</small>
</div>
<TrialBanner />
<div className="rounded-sm pt-2 pb-2 pl-3 pr-2 hover:bg-gray-100 lg:mx-2 lg:pl-2">
<span className="hidden lg:inline">
<UserDropdown />
</span>
<span className="hidden md:inline lg:hidden">
<UserDropdown small />
</span>
</div>
<small style={{ fontSize: "0.5rem" }} className="mx-3 mt-1 mb-2 hidden opacity-50 lg:block">
&copy; {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"}
{process.env.NEXT_PUBLIC_APP_URL === "https://cal.com" ? "h" : "sh"}
<span className="lowercase">-{user && user.plan}</span>
</small>
</div>
</div>
</div>
)}
<div className="flex w-0 flex-1 flex-col overflow-hidden">
<main
className={classNames(
"relative z-0 max-w-[1700px] flex-1 overflow-y-auto focus:outline-none",
"relative z-0 flex-1 overflow-y-auto focus:outline-none",
status === "authenticated" && "max-w-[1700px]",
props.flexChildrenContainer && "flex flex-col"
)}>
{/* show top navigation for md and smaller (tablet and phones) */}
<nav className="flex items-center justify-between border-b border-gray-200 bg-white p-4 md:hidden">
<Link href="/event-types">
<a>
<Logo />
</a>
</Link>
<div className="flex items-center gap-3 self-center">
<button className="rounded-full bg-white p-2 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
<span className="sr-only">{t("view_notifications")}</span>
<Link href="/settings/profile">
<a>
<CogIcon className="h-6 w-6" aria-hidden="true" />
</a>
</Link>
</button>
<UserDropdown small />
</div>
</nav>
{status === "authenticated" && (
<nav className="flex items-center justify-between border-b border-gray-200 bg-white p-4 md:hidden">
<Link href="/event-types">
<a>
<Logo />
</a>
</Link>
<div className="flex items-center gap-3 self-center">
<button className="rounded-full bg-white p-2 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
<span className="sr-only">{t("view_notifications")}</span>
<Link href="/settings/profile">
<a>
<CogIcon className="h-6 w-6" aria-hidden="true" />
</a>
</Link>
</button>
<UserDropdown small />
</div>
</nav>
)}
<div
className={classNames(
props.centered && "mx-auto md:max-w-5xl",
props.flexChildrenContainer && "flex flex-1 flex-col",
"py-8"
!props.large && "py-8"
)}>
{!!props.backPath && (
<div className="mx-3 mb-8 sm:mx-8">
@ -306,14 +350,22 @@ export default function Shell(props: {
</Button>
</div>
)}
<div className="block justify-between px-4 sm:flex sm:px-6 md:px-8">
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
<div className="mb-8 w-full">
<h1 className="font-cal mb-1 text-xl text-gray-900">{props.heading}</h1>
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
{props.heading && props.subtitle && (
<div
className={classNames(
props.large && "bg-gray-100 py-8 lg:mb-8 lg:pt-16 lg:pb-7",
"block min-h-[80px] justify-between px-4 sm:flex sm:px-6 md:px-8"
)}>
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
<div className="mb-8 w-full">
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
{props.heading}
</h1>
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
</div>
{props.CTA && <div className="mb-4 flex-shrink-0">{props.CTA}</div>}
</div>
<div className="mb-4 flex-shrink-0">{props.CTA}</div>
</div>
)}
<div
className={classNames(
"px-4 sm:px-6 md:px-8",
@ -322,34 +374,36 @@ export default function Shell(props: {
{props.children}
</div>
{/* show bottom navigation for md and smaller (tablet and phones) */}
<nav className="bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden">
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
{navigation.flatMap((item, itemIdx) =>
item.href === "/settings/profile" ? (
[]
) : (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current ? "text-gray-900" : "text-neutral-400 hover:text-gray-700",
itemIdx === 0 ? "rounded-l-lg" : "",
itemIdx === navigation.length - 1 ? "rounded-r-lg" : "",
"group relative min-w-0 flex-1 overflow-hidden bg-white py-2 px-2 text-center text-xs font-medium hover:bg-gray-50 focus:z-10 sm:text-sm"
)}
aria-current={item.current ? "page" : undefined}>
<item.icon
{status === "authenticated" && (
<nav className="bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden">
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
{navigation.flatMap((item, itemIdx) =>
item.href === "/settings/profile" ? (
[]
) : (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
"mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center"
item.current ? "text-gray-900" : "text-neutral-400 hover:text-gray-700",
itemIdx === 0 ? "rounded-l-lg" : "",
itemIdx === navigation.length - 1 ? "rounded-r-lg" : "",
"group relative min-w-0 flex-1 overflow-hidden bg-white py-2 px-2 text-center text-xs font-medium hover:bg-gray-50 focus:z-10 sm:text-sm"
)}
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
</a>
</Link>
)
)}
</nav>
aria-current={item.current ? "page" : undefined}>
<item.icon
className={classNames(
item.current ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
"mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center"
)}
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
</a>
</Link>
)
)}
</nav>
)}
{/* add padding to content for mobile navigation*/}
<div className="block pt-12 md:hidden" />
</div>

View File

@ -0,0 +1,28 @@
import type { App } from "@calcom/types/App";
import { useLocale } from "@lib/hooks/useLocale";
import AppCard from "./AppCard";
export default function AllApps({ apps }: { apps: App[] }) {
const { t } = useLocale();
return (
<div className="mb-16">
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
<div className="grid-col-1 grid gap-3 md:grid-cols-3">
{apps.map((app) => (
<AppCard
key={app.name}
name={app.name}
slug={app.slug}
description={app.description}
logo={app.logo}
rating={app.rating}
reviews={app.reviews}
/>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,41 @@
import { StarIcon } from "@heroicons/react/solid";
import Link from "next/link";
import Button from "@calcom/ui/Button";
interface AppCardProps {
logo: string;
name: string;
slug?: string;
category?: string;
description: string;
rating: number;
reviews?: number;
}
export default function AppCard(props: AppCardProps) {
return (
<Link href={"/apps/" + props.slug}>
<a className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50">
<div className="flex">
<img src={props.logo} alt={props.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
<Button
color="secondary"
className="ml-auto flex self-start"
onClick={() => {
// TODO: Actually add the integration
console.log("The magic is supposed to happen here");
}}>
Add
</Button>
</div>
<h3 className="font-medium">{props.name}</h3>
<div className="flex text-sm text-gray-800">
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
</div>
<p className="mt-2 text-sm text-gray-500">{props.description}</p>
</a>
</Link>
);
}

View File

@ -0,0 +1,28 @@
import { CreditCardIcon } from "@heroicons/react/outline";
import Link from "next/link";
import { useLocale } from "@lib/hooks/useLocale";
export default function AppStoreCategories(props: any) {
const { t } = useLocale();
return (
<div className="mb-16">
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("popular_categories")}</h2>
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
{props.categories.map((category: any) => (
<Link key={category.name} href={"/apps/categories/" + category.name}>
<a className="flex rounded-sm bg-gray-100 px-6 py-4">
<div className="mr-4 flex h-12 w-12 rounded-sm bg-white">
<CreditCardIcon className="mx-auto h-6 w-6 self-center" />
</div>
<div>
<h3 className="font-medium capitalize">{category.name}</h3>
<p className="text-sm text-gray-500">{category.count} apps</p>
</div>
</a>
</Link>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,88 @@
import Glide from "@glidejs/glide";
import "@glidejs/glide/dist/css/glide.core.min.css";
import "@glidejs/glide/dist/css/glide.theme.min.css";
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/solid";
import { useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import useMediaQuery from "@lib/hooks/useMediaQuery";
import AppCard from "./AppCard";
const Slider = <T extends App>({ items }: { items: T[] }) => {
const { t } = useLocale();
const isMobile = useMediaQuery("(max-width: 767px)");
const [size, setSize] = useState(3);
useEffect(() => {
if (isMobile) {
setSize(1);
} else {
setSize(3);
}
}, [isMobile]);
useEffect(() => {
const slider = new Glide(".glide", {
type: "carousel",
perView: size,
});
slider.mount();
// @ts-ignore TODO: This method is missing in types
return () => slider.destroy();
}, [size]);
return (
<div className="mb-16">
<style jsx global>
{`
.glide__slide {
height: auto !important;
}
`}
</style>
<div className="glide">
<div className="flex cursor-default">
<div>
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("trending_apps")}</h2>
</div>
<div className="glide__arrows ml-auto" data-glide-el="controls">
<button data-glide-dir="<" className="mr-4">
<ArrowLeftIcon className="h-5 w-5 text-gray-600 hover:text-black" />
</button>
<button data-glide-dir=">">
<ArrowRightIcon className="h-5 w-5 text-gray-600 hover:text-black" />
</button>
</div>
</div>
<div className="glide__track" data-glide-el="track">
<ul className="glide__slides">
{items.map((app) => {
return (
app.trending && (
<li key={app.name} className="glide__slide h-auto">
<AppCard
key={app.name}
name={app.name}
slug={app.slug}
description={app.description}
logo={app.logo}
rating={app.rating}
reviews={app.reviews}
/>
</li>
)
);
})}
</ul>
</div>
</div>
</div>
);
};
export default Slider;

View File

@ -132,6 +132,7 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
const locationInfo = (type: LocationType) => locations.find((location) => location.type === type);
// TODO: Move to translations
// Also TODO: Get these dynamically from App Store
const locationLabels = {
[LocationType.InPerson]: t("in_person_meeting"),
[LocationType.Phone]: t("phone_call"),
@ -141,6 +142,7 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
[LocationType.Daily]: "Daily.co Video",
[LocationType.Huddle01]: "Huddle01 Video",
[LocationType.Tandem]: "Tandem Video",
[LocationType.Teams]: "MS Teams",
};
const loggedInIsOwner = eventType?.users[0]?.name === session?.user?.name;
const defaultValues = () => {

View File

@ -1,6 +1,7 @@
import React, { Fragment } from "react";
import { Fragment } from "react";
import { useMutation } from "react-query";
import { InstallAppButton } from "@calcom/app-store/components";
import showToast from "@calcom/lib/notification";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
@ -14,7 +15,6 @@ import DestinationCalendarSelector from "@components/DestinationCalendarSelector
import { List } from "@components/List";
import { ShellSubHeading } from "@components/Shell";
import ConnectIntegration from "./ConnectIntegrations";
import DisconnectIntegration from "./DisconnectIntegration";
import IntegrationListItem from "./IntegrationListItem";
import SubHeadingTitleWithConnections from "./SubHeadingTitleWithConnections";
@ -110,7 +110,8 @@ function ConnectedCalendarsList(props: Props) {
<Fragment key={item.credentialId}>
{item.calendars ? (
<IntegrationListItem
{...item.integration}
title={item.integration.title}
imageSrc={item.integration.imageSrc}
description={item.primary?.externalId || "No external Id"}
actions={
<DisconnectIntegration
@ -127,8 +128,8 @@ function ConnectedCalendarsList(props: Props) {
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
externalId={cal.externalId as string}
title={cal.name as string}
externalId={cal.externalId}
title={cal.name || "Nameless calendar"}
type={item.integration.type}
defaultSelected={cal.isSelected}
/>
@ -174,16 +175,18 @@ function CalendarList(props: Props) {
{data.calendar.items.map((item) => (
<IntegrationListItem
key={item.title}
{...item}
title={item.title}
imageSrc={item.imageSrc}
description={item.description}
actions={
<ConnectIntegration
<InstallAppButton
type={item.type}
render={(btnProps) => (
<Button color="secondary" {...btnProps} data-testid="integration-connection-button">
render={(buttonProps) => (
<Button color="secondary" {...buttonProps}>
{t("connect")}
</Button>
)}
onOpenChange={() => props.onChanged()}
onChanged={() => props.onChanged()}
/>
}
/>
@ -193,6 +196,7 @@ function CalendarList(props: Props) {
/>
);
}
export function CalendarListContainer(props: { heading?: false }) {
const { t } = useLocale();
const { heading = true } = props;

View File

@ -1,64 +0,0 @@
import type { IntegrationOAuthCallbackState } from "pages/api/integrations/types";
import { useState } from "react";
import { useMutation } from "react-query";
import { ButtonBaseProps } from "@calcom/ui/Button";
import { NEXT_PUBLIC_BASE_URL } from "@lib/config/constants";
import { AddAppleIntegrationModal } from "@lib/integrations/calendar/components/AddAppleIntegration";
import { AddCalDavIntegrationModal } from "@lib/integrations/calendar/components/AddCalDavIntegration";
export default function ConnectIntegration(props: {
type: string;
render: (renderProps: ButtonBaseProps) => JSX.Element;
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
}) {
const { type } = props;
const [isLoading, setIsLoading] = useState(false);
const mutation = useMutation(async () => {
const state: IntegrationOAuthCallbackState = {
returnTo: NEXT_PUBLIC_BASE_URL + location.pathname + location.search,
};
const stateStr = encodeURIComponent(JSON.stringify(state));
const searchParams = `?state=${stateStr}`;
const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add" + searchParams);
if (!res.ok) {
throw new Error("Something went wrong");
}
const json = await res.json();
window.location.href = json.url;
setIsLoading(true);
});
const [isModalOpen, _setIsModalOpen] = useState(false);
const setIsModalOpen = (v: boolean) => {
_setIsModalOpen(v);
props.onOpenChange(v);
};
return (
<>
{props.render({
onClick() {
if (["caldav_calendar", "apple_calendar"].includes(type)) {
// special handlers
setIsModalOpen(true);
return;
}
mutation.mutate();
},
loading: mutation.isLoading || isLoading,
disabled: isModalOpen,
})}
{type === "caldav_calendar" && (
<AddCalDavIntegrationModal open={isModalOpen} onOpenChange={setIsModalOpen} />
)}
{type === "apple_calendar" && (
<AddAppleIntegrationModal open={isModalOpen} onOpenChange={setIsModalOpen} />
)}
</>
);
}

View File

@ -1,6 +1,7 @@
import { useState } from "react";
import { useMutation } from "react-query";
import showToast from "@calcom/lib/notification";
import { ButtonBaseProps } from "@calcom/ui/Button";
import { Dialog } from "@calcom/ui/Dialog";
@ -25,12 +26,14 @@ export default function DisconnectIntegration(props: {
if (!res.ok) {
throw new Error("Something went wrong");
}
return res.json();
},
{
async onSettled() {
await props.onOpenChange(modalOpen);
},
onSuccess() {
onSuccess(data) {
showToast(data.message, "success");
setModalOpen(false);
},
}

View File

@ -1,4 +1,3 @@
import Image from "next/image";
import { ReactNode } from "react";
import classNames from "@lib/classNames";
@ -6,7 +5,7 @@ import classNames from "@lib/classNames";
import { ListItem, ListItemText, ListItemTitle } from "@components/List";
function IntegrationListItem(props: {
imageSrc: string;
imageSrc?: string;
title: string;
description: string;
actions?: ReactNode;
@ -15,7 +14,7 @@ function IntegrationListItem(props: {
return (
<ListItem expanded={!!props.children} className={classNames("flex-col")}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
<Image width={40} height={40} src={`/${props.imageSrc}`} alt={props.title} />
{props.imageSrc && <img className="h-10 w-10" src={props.imageSrc} alt={props.title} />}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">{props.title}</ListItemTitle>
<ListItemText component="p">{props.description}</ListItemText>

View File

@ -16,7 +16,7 @@ export const DatePicker = ({ date, onDatesChange, className }: Props) => {
return (
<PrimitiveDatePicker
className={classNames(
"focus:border-primary-500 focus:ring-primary-500 rounded-sm border border-gray-300 p-1 pl-2 shadow-sm sm:text-sm",
"focus:ring-primary-500 focus:border-primary-500 rounded-sm border border-gray-300 p-1 pl-2 shadow-sm sm:text-sm",
className
)}
clearIcon={null}

View File

@ -43,7 +43,7 @@ export default function WebhookListContainer(props: WebhookListContainerType) {
<ListItem className={classNames("flex-col")}>
<div
className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
<Image width={40} height={40} src="/integrations/webhooks.svg" alt="Webhooks" />
<Image width={40} height={40} src="/apps/webhooks.svg" alt="Webhooks" />
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">Webhooks</ListItemTitle>
<ListItemText component="p">{t("automation")}</ListItemText>

View File

@ -74,7 +74,7 @@ export default function TeamAvailabilityModal(props: Props) {
]}
isSearchable={false}
classNamePrefix="react-select"
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
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)}
/>

View File

@ -47,7 +47,7 @@ export default function TeamAvailabilityScreen(props: Props) {
<Avatar
imageSrc={getPlaceholderAvatar(member?.avatar, member?.name as string)}
alt={member?.name || ""}
className="min-h-10 min-w-10 mt-1 h-10 w-10 rounded-full"
className="min-w-10 min-h-10 mt-1 h-10 w-10 rounded-full"
/>
<div className="ml-3 inline-block overflow-hidden pt-1">
<span className="truncate text-lg font-bold text-neutral-700">{member?.name}</span>
@ -93,7 +93,7 @@ export default function TeamAvailabilityScreen(props: Props) {
]}
isSearchable={false}
classNamePrefix="react-select"
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
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)}
/>

View File

@ -94,7 +94,7 @@ const CryptoSection = (props: CryptoSectionProps) => {
const verifyButton = useMemo(() => {
return (
<Button color="secondary" onClick={verifyWallet} type="button" id="hasToken" name="hasToken">
<img className="mr-1 h-5" src="/integrations/metamask.svg" />
<img className="mr-1 h-5" src="/apps/metamask.svg" />
{t("verify_wallet")}
</Button>
);
@ -103,7 +103,7 @@ const CryptoSection = (props: CryptoSectionProps) => {
const connectButton = useMemo(() => {
return (
<Button color="secondary" onClick={connectMetamask} type="button">
<img className="mr-1 h-5" src="/integrations/metamask.svg" />
<img className="mr-1 h-5" src="/apps/metamask.svg" />
{t("connect_metamask")}
</Button>
);
@ -118,7 +118,7 @@ const CryptoSection = (props: CryptoSectionProps) => {
await connectMetamask();
await verifyWallet();
}}>
<img className="mr-1 h-5" src="/integrations/metamask.svg" />
<img className="mr-1 h-5" src="/apps/metamask.svg" />
{t("verify_wallet")}
</Button>
);

View File

@ -6,9 +6,9 @@ import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
import { createPaymentLink } from "@calcom/stripe/client";
import stripe, { PaymentData } from "@calcom/stripe/server";
import { CalendarEvent } from "@calcom/types/Calendar";
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
export type PaymentInfo = {
link?: string | null;

View File

@ -2,14 +2,14 @@ import { buffer } from "micro";
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
import EventManager from "@calcom/core/EventManager";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
import stripe from "@calcom/stripe/server";
import { CalendarEvent } from "@calcom/types/Calendar";
import { IS_PRODUCTION } from "@lib/config/constants";
import { HttpError as HttpCode } from "@lib/core/http/error";
import EventManager from "@lib/events/EventManager";
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
import prisma from "@lib/prisma";
import { getTranslation } from "@server/lib/i18n";
@ -138,6 +138,11 @@ const webhookHandlers: Record<string, WebhookHandler | undefined> = {
"payment_intent.succeeded": handlePaymentSuccess,
};
/**
* @deprecated
* We need to create a PaymentManager in `@calcom/core`
* to prevent circular dependencies on App Store migration
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "POST") {

View File

@ -1,6 +1,2 @@
export const BASE_URL = process.env.BASE_URL || `https://${process.env.VERCEL_URL}`;
export const WEBSITE_URL = process.env.NEXT_PUBLIC_APP_URL || "https://cal.com";
export const IS_PRODUCTION = process.env.NODE_ENV === "production";
export const TRIAL_LIMIT_DAYS = 14;
export const HOSTED_CAL_FEATURES = process.env.HOSTED_CAL_FEATURES || BASE_URL === "https://app.cal.com";
export const NEXT_PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || `https://${process.env.VERCEL_URL}`;
// TODO: Remove this file once everything is imported from `@calcom/lib`
export * from "@calcom/lib/constants";

View File

@ -1,3 +1,5 @@
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email";
import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email";
import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email";
@ -12,7 +14,6 @@ import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-reque
import OrganizerRescheduledEmail from "@lib/emails/templates/organizer-rescheduled-email";
import OrganizerScheduledEmail from "@lib/emails/templates/organizer-scheduled-email";
import TeamInviteEmail, { TeamInvite } from "@lib/emails/templates/team-invite-email";
import { CalendarEvent, Person } from "@lib/integrations/calendar/interfaces/Calendar";
export const sendScheduledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];

View File

@ -4,7 +4,7 @@ import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { getCancelLink } from "@lib/CalEventParser";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
import {

View File

@ -6,11 +6,11 @@ import utc from "dayjs/plugin/utc";
import { createEvent, DateArray } from "ics";
import nodemailer from "nodemailer";
import { getAppName } from "@calcom/app-store/utils";
import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import type { Person, CalendarEvent } from "@calcom/types/Calendar";
import { getCancelLink, getRichDescription } from "@lib/CalEventParser";
import { getIntegrationName } from "@lib/integrations";
import { CalendarEvent, Person } from "@lib/integrations/calendar/interfaces/Calendar";
import { serverConfig } from "@lib/serverConfig";
import {
@ -309,7 +309,7 @@ ${getRichDescription(this.calEvent)}
}
protected getLocation(): string {
let providerName = this.calEvent.location ? getIntegrationName(this.calEvent.location) : "";
let providerName = this.calEvent.location ? getAppName(this.calEvent.location) : "";
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
const location = this.calEvent.location.split(":")[1];

View File

@ -4,7 +4,7 @@ import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { getCancelLink } from "@lib/CalEventParser";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import {
emailHead,

View File

@ -6,11 +6,11 @@ import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import nodemailer from "nodemailer";
import { getAppName } from "@calcom/app-store/utils";
import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { getCancelLink, getRichDescription } from "@lib/CalEventParser";
import { getIntegrationName } from "@lib/integrations";
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
import { serverConfig } from "@lib/serverConfig";
import {
@ -300,7 +300,7 @@ ${getRichDescription(this.calEvent)}
}
protected getLocation(): string {
let providerName = this.calEvent.location ? getIntegrationName(this.calEvent.location) : "";
let providerName = this.calEvent.location ? getAppName(this.calEvent.location) : "";
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
const location = this.calEvent.location.split(":")[1];

View File

@ -0,0 +1,19 @@
import { useState, useEffect } from "react";
const useMediaQuery = (query: string) => {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => setMatches(media.matches);
window.addEventListener("resize", listener);
return () => window.removeEventListener("resize", listener);
}, [matches, query]);
return matches;
};
export default useMediaQuery;

View File

@ -1,34 +0,0 @@
export function getIntegrationName(name: string) {
switch (name) {
case "google_calendar":
return "Google Calendar";
case "office365_calendar":
return "Office 365 Calendar";
case "zoom_video":
return "Zoom";
case "caldav_calendar":
return "CalDav Server";
case "stripe_payment":
return "Stripe";
case "apple_calendar":
return "Apple Calendar";
case "daily_video":
return "Daily";
case "jitsi_video":
return "Jitsi Meet";
case "huddle01_video":
return "Huddle01";
case "tandem_video":
return "Tandem";
}
}
export function getIntegrationType(name: string): string {
if (name.endsWith("_calendar")) {
return "Calendar";
}
if (name.endsWith("_payment")) {
return "Payment";
}
return "Unknown";
}

View File

@ -1 +0,0 @@
export const TIMEZONE_FORMAT = "YYYY-MM-DDTHH:mm:ss[Z]";

View File

@ -1,10 +0,0 @@
export const CALDAV_CALENDAR_TYPE = "caldav";
export const APPLE_CALENDAR_URL = "https://caldav.icloud.com";
export const CALENDAR_INTEGRATIONS_TYPES = {
apple: "apple_calendar",
caldav: "caldav_calendar",
google: "google_calendar",
office365: "office365_calendar",
};

View File

@ -1,57 +0,0 @@
import dayjs from "dayjs";
import ICAL from "ical.js";
import AppleCalendarService from "../services/AppleCalendarService";
import CalDavCalendarService from "../services/CalDavCalendarService";
import GoogleCalendarService from "../services/GoogleCalendarService";
import Office365CalendarService from "../services/Office365CalendarService";
export type EventBusyDate = Record<"start" | "end", Date | string>;
export type CalendarServiceType =
| typeof AppleCalendarService
| typeof CalDavCalendarService
| typeof GoogleCalendarService
| typeof Office365CalendarService;
export type NewCalendarEventType = {
uid: string;
id: string;
type: string;
password: string;
url: string;
additionalInfo: Record<string, any>;
};
export type CalendarEventType = {
uid: string;
etag: string;
/** This is the actual caldav event url, not the location url. */
url: string;
summary: string;
description: string;
location: string;
sequence: number;
startDate: Date | dayjs.Dayjs;
endDate: Date | dayjs.Dayjs;
duration: {
weeks: number;
days: number;
hours: number;
minutes: number;
seconds: number;
isNegative: boolean;
};
organizer: string;
attendees: any[][];
recurrenceId: ICAL.Time;
timezone: any;
};
export type BatchResponse = {
responses: SubResponse[];
};
export type SubResponse = {
body: { value: { start: { dateTime: string }; end: { dateTime: string } }[] };
};

View File

@ -1,5 +0,0 @@
import { calendar_v3 } from "googleapis";
export interface ConferenceData {
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
}

View File

@ -1,10 +0,0 @@
import { Credential } from "@prisma/client";
import { APPLE_CALENDAR_URL, CALENDAR_INTEGRATIONS_TYPES } from "../constants/generals";
import CalendarService from "./BaseCalendarService";
export default class AppleCalendarService extends CalendarService {
constructor(credential: Credential) {
super(credential, CALENDAR_INTEGRATIONS_TYPES.apple, APPLE_CALENDAR_URL);
}
}

View File

@ -1,10 +0,0 @@
import { Credential } from "@prisma/client";
import { CALENDAR_INTEGRATIONS_TYPES } from "../constants/generals";
import CalendarService from "./BaseCalendarService";
export default class CalDavCalendarService extends CalendarService {
constructor(credential: Credential) {
super(credential, CALENDAR_INTEGRATIONS_TYPES.caldav);
}
}

View File

@ -1,16 +0,0 @@
import dayjs from "dayjs";
import { Attendee, DateArray, DurationObject, Person } from "ics";
export const convertDate = (date: string): DateArray =>
dayjs(date)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray;
export const getDuration = (start: string, end: string): DurationObject => ({
minutes: dayjs(end).diff(dayjs(start), "minute"),
});
export const getAttendees = (attendees: Person[]): Attendee[] =>
attendees.map(({ email, name }) => ({ name, email, partstat: "NEEDS-ACTION" }));

View File

@ -1,164 +0,0 @@
import { Prisma } from "@prisma/client";
import _ from "lodash";
import { validJson } from "@lib/jsonUtils";
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
select: { id: true, type: true },
});
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
export type Integration = {
installed: boolean;
type:
| "google_calendar"
| "office365_calendar"
| "zoom_video"
| "daily_video"
| "tandem_video"
| "caldav_calendar"
| "apple_calendar"
| "stripe_payment"
| "jitsi_video"
| "huddle01_video"
| "metamask_web3";
title: string;
imageSrc: string;
description: string;
variant: "calendar" | "conferencing" | "payment" | "web3";
};
export const ALL_INTEGRATIONS = [
{
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
type: "google_calendar",
title: "Google Calendar",
imageSrc: "integrations/google-calendar.svg",
description: "For personal and business calendars",
variant: "calendar",
},
{
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
type: "office365_calendar",
title: "Office 365 / Outlook.com Calendar",
imageSrc: "integrations/outlook.svg",
description: "For personal and business calendars",
variant: "calendar",
},
{
installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
type: "zoom_video",
title: "Zoom",
imageSrc: "integrations/zoom.svg",
description: "Video Conferencing",
variant: "conferencing",
},
{
installed: !!process.env.DAILY_API_KEY,
type: "daily_video",
title: "Daily.co Video",
imageSrc: "integrations/daily.svg",
description: "Video Conferencing",
variant: "conferencing",
},
{
installed: true,
type: "jitsi_video",
title: "Jitsi Meet",
imageSrc: "integrations/jitsi.svg",
description: "Video Conferencing",
variant: "conferencing",
},
{
installed: true,
type: "huddle01_video",
title: "Huddle01",
imageSrc: "integrations/huddle.svg",
description: "Video Conferencing",
variant: "conferencing",
},
{
installed: !!(process.env.TANDEM_CLIENT_ID && process.env.TANDEM_CLIENT_SECRET),
type: "tandem_video",
title: "Tandem Video",
imageSrc: "integrations/tandem.svg",
description: "Virtual Office | Video Conferencing",
variant: "conferencing",
},
{
installed: true,
type: "caldav_calendar",
title: "CalDav Server",
imageSrc: "integrations/caldav.svg",
description: "For personal and business calendars",
variant: "calendar",
},
{
installed: true,
type: "apple_calendar",
title: "Apple Calendar",
imageSrc: "integrations/apple-calendar.svg",
description: "For personal and business calendars",
variant: "calendar",
},
{
installed: !!(
process.env.STRIPE_CLIENT_ID &&
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
process.env.STRIPE_PRIVATE_KEY
),
type: "stripe_payment",
title: "Stripe",
imageSrc: "integrations/stripe.svg",
description: "Collect payments",
variant: "payment",
},
{
installed: true,
type: "metamask_web3",
title: "Metamask",
imageSrc: "integrations/apple-calendar.svg",
description: "For personal and business calendars",
variant: "web3",
},
] as Integration[];
function getIntegrations(userCredentials: CredentialData[]) {
const integrations = ALL_INTEGRATIONS.map((integration) => {
const credentials = userCredentials
.filter((credential) => credential.type === integration.type)
.map((credential) => _.pick(credential, ["id", "type"])); // ensure we don't leak `key` to frontend
const credential: typeof credentials[number] | null = credentials[0] || null;
return {
...integration,
/**
* @deprecated use `credentials`
*/
credential,
credentials,
};
});
return integrations;
}
export type IntegrationMeta = ReturnType<typeof getIntegrations>;
export function hasIntegration(integrations: IntegrationMeta, type: string): boolean {
return !!integrations.find(
(i) =>
i.type === type &&
!!i.installed &&
(type === "daily_video" ||
type === "jitsi_video" ||
type === "huddle01_video" ||
i.credentials.length > 0)
);
}
export function hasIntegrationInstalled(type: Integration["type"]): boolean {
return ALL_INTEGRATIONS.some((i) => i.type === type && !!i.installed);
}
export default getIntegrations;

View File

@ -1,10 +1,10 @@
// import { getBusyVideoTimes } from "@lib/videoClient";
import { Prisma } from "@prisma/client";
import dayjs from "dayjs";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
import prisma from "@lib/prisma";
export async function getUserAvailability(query: {
@ -67,8 +67,6 @@ export async function getUserAvailability(query: {
selectedCalendars
);
// busyTimes.push(...await getBusyVideoTimes(currentUser.credentials, dateFrom.format(), dateTo.format()));
const bufferedBusyTimes = busyTimes.map((a) => ({
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),

View File

@ -1,9 +1,2 @@
export const randomString = function (length = 12) {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
// TODO: Remove this file once everything is imported from `@calcom/lib`
export * from "@calcom/lib/random";

View File

@ -6,7 +6,7 @@
*/
export const isBrowserLocale24h = () => {
let locale = "en-US";
if (process.browser && navigator) locale = navigator?.language;
if (typeof window !== "undefined" && navigator) locale = navigator?.language;
return !new Intl.DateTimeFormat(locale, { hour: "numeric" }).format(0).match(/AM/);
};
export const detectBrowserTimeFormat = isBrowserLocale24h() ? "H:mm" : "h:mma";

View File

@ -1,6 +1,6 @@
import { compile } from "handlebars";
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
import type { CalendarEvent } from "@calcom/types/Calendar";
type ContentType = "application/json" | "application/x-www-form-urlencoded";

View File

@ -1,4 +1,6 @@
const withTM = require("next-transpile-modules")([
"@calcom/app-store",
"@calcom/core",
"@calcom/ee",
"@calcom/lib",
"@calcom/prisma",

View File

@ -27,6 +27,8 @@
},
"dependencies": {
"@boxyhq/saml-jackson": "0.3.6",
"@calcom/app-store": "*",
"@calcom/core": "*",
"@calcom/ee": "*",
"@calcom/lib": "*",
"@calcom/prisma": "*",
@ -34,6 +36,7 @@
"@calcom/tsconfig": "*",
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.21.0",
"@glidejs/glide": "^3.5.2",
"@heroicons/react": "^1.0.5",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.8.5",
@ -100,19 +103,19 @@
"short-uuid": "^4.2.0",
"stripe": "^8.191.0",
"superjson": "1.8.1",
"tsdav": "2.0.0",
"tslog": "^3.2.1",
"uuid": "^8.3.2",
"web3": "^1.6.1",
"zod": "^3.8.2"
},
"devDependencies": {
"@calcom/config": "*",
"@calcom/types": "*",
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
"@playwright/test": "^1.18.1",
"@types/accept-language-parser": "1.5.2",
"@types/async": "^3.2.10",
"@types/bcryptjs": "^2.4.2",
"@types/glidejs__glide": "^3.4.1",
"@types/jest": "^27.0.3",
"@types/lodash": "^4.14.177",
"@types/micro": "^7.3.6",

View File

@ -7,9 +7,9 @@ import NextError, { ErrorProps } from "next/error";
import React from "react";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import { HttpError } from "@lib/core/http/error";
import logger from "@lib/logger";
import { ErrorPage } from "@components/error/error-page";

View File

@ -5,8 +5,9 @@ import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import { authenticator } from "otplib";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { ErrorCode, verifyPassword } from "@lib/auth";
import { symmetricDecrypt } from "@lib/crypto";
import prisma from "@lib/prisma";
import { randomString } from "@lib/random";
import { isSAMLLoginEnabled, samlLoginUrl, hostedCal } from "@lib/saml";

View File

@ -1,8 +1,9 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { ErrorCode, getSession } from "@lib/auth";
import { symmetricDecrypt } from "@lib/crypto";
import prisma from "@lib/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@ -3,8 +3,9 @@ import { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import qrcode from "qrcode";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
import { symmetricEncrypt } from "@lib/crypto";
import prisma from "@lib/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@ -1,13 +1,14 @@
// import { getBusyVideoTimes } from "@lib/videoClient";
// import { getBusyVideoTimes } from "@calcom/core/videoClient";
import { Prisma } from "@prisma/client";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import type { NextApiRequest, NextApiResponse } from "next";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
import prisma from "@lib/prisma";
dayjs.extend(utc);

View File

@ -1,8 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import notEmpty from "@calcom/lib/notEmpty";
import { getSession } from "@lib/auth";
import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations/calendar/CalendarManager";
import notEmpty from "@lib/notEmpty";
import prisma from "@lib/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@ -1,15 +1,16 @@
import { Prisma, User, Booking, SchedulingType, BookingStatus } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import EventManager from "@calcom/core/EventManager";
import logger from "@calcom/lib/logger";
import type { AdditionInformation } from "@calcom/types/Calendar";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { sendDeclinedEmails } from "@lib/emails/email-manager";
import { sendScheduledEmails } from "@lib/emails/email-manager";
import EventManager from "@lib/events/EventManager";
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
import logger from "@lib/logger";
import prisma from "@lib/prisma";
import { BookingConfirmBody } from "@lib/types/booking";

View File

@ -9,27 +9,27 @@ import type { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import EventManager from "@calcom/core/EventManager";
import { getBusyVideoTimes } from "@calcom/core/videoClient";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import notEmpty from "@calcom/lib/notEmpty";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type { AdditionInformation, CalendarEvent, EventBusyDate } from "@calcom/types/Calendar";
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import { handlePayment } from "@ee/lib/stripe/server";
import {
sendScheduledEmails,
sendRescheduledEmails,
sendOrganizerRequestEmail,
sendAttendeeRequestEmail,
sendOrganizerRequestEmail,
sendRescheduledEmails,
sendScheduledEmails,
} from "@lib/emails/email-manager";
import { ensureArray } from "@lib/ensureArray";
import { getEventName } from "@lib/event";
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
import { EventBusyDate } from "@lib/integrations/calendar/constants/types";
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
import logger from "@lib/logger";
import notEmpty from "@lib/notEmpty";
import prisma from "@lib/prisma";
import { BookingCreateBody } from "@lib/types/booking";
import { getBusyVideoTimes } from "@lib/videoClient";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscribers from "@lib/webhooks/subscriptions";

View File

@ -1,18 +1,18 @@
import { BookingStatus, WebhookTriggerEvents } from "@prisma/client";
import { BookingStatus, Credential, WebhookTriggerEvents } from "@prisma/client";
import async from "async";
import dayjs from "dayjs";
import { NextApiRequest, NextApiResponse } from "next";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getCalendar } from "@calcom/core/CalendarManager";
import { deleteMeeting } from "@calcom/core/videoClient";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { sendCancelledEmails } from "@lib/emails/email-manager";
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
import prisma from "@lib/prisma";
import { deleteMeeting } from "@lib/videoClient";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscribers from "@lib/webhooks/subscriptions";
@ -159,11 +159,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
/** TODO: Remove this without breaking functionality */
if (bookingToDelete.location === "integrations:daily") {
bookingToDelete.user.credentials.push(FAKE_DAILY_CREDENTIAL);
}
const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential) => {
const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential: Credential) => {
const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0]?.uid;
if (bookingRefUid) {
if (credential.type.endsWith("_calendar")) {

View File

@ -2,8 +2,9 @@ import { ReminderType } from "@prisma/client";
import dayjs from "dayjs";
import type { NextApiRequest, NextApiResponse } from "next";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { sendOrganizerRequestReminderEmail } from "@lib/emails/email-manager";
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
import prisma from "@lib/prisma";
import { getTranslation } from "@server/lib/i18n";

View File

@ -0,0 +1,46 @@
import { NextApiRequest, NextApiResponse } from "next";
import appStore from "@calcom/app-store";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Check that user is authenticated
req.session = await getSession({ req });
if (!req.session?.user?.id) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
const { args } = req.query;
if (!Array.isArray(args)) {
return res.status(404).json({ message: `API route not found` });
}
const [_appName, apiEndpoint] = args;
const appName = _appName.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
try {
// TODO: Find a way to dynamically import these modules
// const app = (await import(`@calcom/${appName}`)).default;
const handler = appStore[appName].api[apiEndpoint];
if (typeof handler !== "function")
throw new HttpError({ statusCode: 404, message: `API handler not found` });
const response = await handler(req, res);
console.log("response", response);
res.status(200);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
return res.status(error.statusCode).json({ message: error.message });
}
return res.status(404).json({ message: `API handler not found` });
}
};
export default handler;

View File

@ -1 +0,0 @@
export { default } from "@ee/pages/api/integrations/stripepayment/add";

View File

@ -1 +0,0 @@
export { default } from "@ee/pages/api/integrations/stripepayment/callback";

View File

@ -1 +0,0 @@
export { default } from "@ee/pages/api/integrations/stripepayment/portal";

View File

@ -1,3 +0,0 @@
export type IntegrationOAuthCallbackState = {
returnTo: string;
};

View File

@ -1,21 +0,0 @@
import { NextApiRequest } from "next";
import { IntegrationOAuthCallbackState } from "./types";
export function encodeOAuthState(req: NextApiRequest) {
if (typeof req.query.state !== "string") {
return undefined;
}
const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state);
return JSON.stringify(state);
}
export function decodeOAuthState(req: NextApiRequest) {
if (typeof req.query.state !== "string") {
return undefined;
}
const state: IntegrationOAuthCallbackState = JSON.parse(req.query.state);
return state;
}

View File

@ -0,0 +1,68 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticPropsContext } from "next";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import App from "@components/App";
function SingleAppPage({ data }: inferSSRProps<typeof getStaticProps>) {
return (
<App
name={data.name}
isGlobal={data.isGlobal}
type={data.type}
logo={data.logo}
categories={[data.category]}
author="Cal.com"
feeType={data.feeType || "usage-based"}
price={data.price || 0}
commission={data.commission || 0}
docs={data.docsUrl}
website={data.url}
email={data.email}
// tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy"
body={data.description}
/>
);
}
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
const appStore = getAppRegistry();
const paths = appStore.reduce((paths, app) => {
paths.push({ params: { slug: app.slug } });
return paths;
}, [] as GetStaticPathsResult<{ slug: string }>["paths"]);
return {
paths,
fallback: false,
};
};
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
const appStore = getAppRegistry();
if (typeof ctx.params?.slug !== "string") {
return {
notFound: true,
};
}
const singleApp = appStore.find((app) => app.slug === ctx.params?.slug);
if (!singleApp) {
return {
notFound: true,
};
}
return {
props: {
data: singleApp,
},
};
};
export default SingleAppPage;

View File

@ -0,0 +1,70 @@
import { ChevronLeftIcon } from "@heroicons/react/solid";
import { InferGetStaticPropsType } from "next";
import { useRouter } from "next/router";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import Shell from "@components/Shell";
import AppCard from "@components/apps/AppCard";
export default function Apps({ appStore }: InferGetStaticPropsType<typeof getStaticProps>) {
const { t } = useLocale();
const router = useRouter();
return (
<Shell
heading={router.query.category + " - " + t("app_store")}
subtitle={t("app_store_description")}
large>
<div className="mb-8">
<Button color="secondary" href="/apps">
<ChevronLeftIcon className="h-5 w-5" />
</Button>
</div>
<div className="mb-16">
<h2 className="mb-2 text-lg font-semibold text-gray-900">All {router.query.category} apps</h2>
<div className="grid grid-cols-3 gap-3">
{appStore.map((app) => {
return (
app.category === router.query.category && (
<AppCard
key={app.name}
slug={app.slug}
name={app.name}
description={app.description}
logo={app.logo}
rating={app.rating}
/>
)
);
})}
</div>
</div>
</Shell>
);
}
export const getStaticPaths = async () => {
const appStore = getAppRegistry();
const paths = appStore.reduce((categories, app) => {
if (!categories.includes(app.category)) {
categories.push(app.category);
}
return categories;
}, [] as string[]);
return {
paths: paths.map((category) => ({ params: { category } })),
fallback: false,
};
};
export const getStaticProps = async () => {
return {
props: {
appStore: getAppRegistry(),
},
};
};

View File

@ -0,0 +1,40 @@
import { InferGetStaticPropsType } from "next";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import { useLocale } from "@lib/hooks/useLocale";
import AppsShell from "@components/AppsShell";
import Shell from "@components/Shell";
import AllApps from "@components/apps/AllApps";
import AppStoreCategories from "@components/apps/Categories";
import Slider from "@components/apps/Slider";
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getStaticProps>) {
const { t } = useLocale();
return (
<Shell heading={t("app_store")} subtitle={t("app_store_description")} large>
<AppsShell>
<AppStoreCategories categories={categories} />
<Slider items={appStore} />
<AllApps apps={appStore} />
</AppsShell>
</Shell>
);
}
export const getStaticProps = async () => {
const appStore = getAppRegistry();
const categories = appStore.reduce((c, app) => {
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
return c;
}, {} as Record<string, number>);
return {
props: {
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
appStore,
},
};
};

View File

@ -3,7 +3,9 @@ import Image from "next/image";
import React, { useEffect, useState } from "react";
import { JSONObject } from "superjson/dist/types";
import { InstallAppButton } from "@calcom/app-store/components";
import showToast from "@calcom/lib/notification";
import { App } from "@calcom/types/App";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
@ -18,7 +20,6 @@ import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
import Loader from "@components/Loader";
import Shell, { ShellSubHeading } from "@components/Shell";
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
import IntegrationListItem from "@components/integrations/IntegrationListItem";
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
@ -41,7 +42,7 @@ function IframeEmbedContainer() {
<List>
<ListItem className={classNames("flex-col")}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
<Image width={40} height={40} src="/integrations/embed.svg" alt="Embed" />
<Image width={40} height={40} src="/apps/embed.svg" alt="Embed" />
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">{t("standard_iframe")}</ListItemTitle>
<ListItemText component="p">{t("embed_your_calendar")}</ListItemText>
@ -66,7 +67,7 @@ function IframeEmbedContainer() {
</ListItem>
<ListItem className={classNames("flex-col")}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
<Image width={40} height={40} src="/integrations/embed.svg" alt="Embed" />
<Image width={40} height={40} src="/apps/embed.svg" alt="Embed" />
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">{t("responsive_fullscreen_iframe")}</ListItemTitle>
<ListItemText component="p">A fullscreen scheduling experience on your website</ListItemText>
@ -108,7 +109,8 @@ function IframeEmbedContainer() {
function ConnectOrDisconnectIntegrationButton(props: {
//
credentialIds: number[];
type: string;
type: App["type"];
isGlobal?: boolean;
installed: boolean;
}) {
const { t } = useLocale();
@ -139,7 +141,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
);
}
/** We don't need to "Connect", just show that it's installed */
if (["daily_video", "huddle01_video", "jitsi_video"].includes(props.type)) {
if (props.isGlobal) {
return (
<div className="truncate px-3 py-2">
<h3 className="text-sm font-medium text-gray-700">{t("installed")}</h3>
@ -147,14 +149,14 @@ function ConnectOrDisconnectIntegrationButton(props: {
);
}
return (
<ConnectIntegration
<InstallAppButton
type={props.type}
render={(btnProps) => (
<Button color="secondary" {...btnProps} data-testid="integration-connection-button">
render={(buttonProps) => (
<Button color="secondary" {...buttonProps} data-testid="integration-connection-button">
{t("connect")}
</Button>
)}
onOpenChange={handleOpenChange}
onChanged={handleOpenChange}
/>
);
}
@ -179,8 +181,17 @@ function IntegrationsContainer() {
{data.conferencing.items.map((item) => (
<IntegrationListItem
key={item.title}
{...item}
actions={<ConnectOrDisconnectIntegrationButton {...item} />}
title={item.title}
imageSrc={item.imageSrc}
description={item.description}
actions={
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed={item.installed}
/>
}
/>
))}
</List>
@ -195,8 +206,17 @@ function IntegrationsContainer() {
{data.payment.items.map((item) => (
<IntegrationListItem
key={item.title}
{...item}
actions={<ConnectOrDisconnectIntegrationButton {...item} />}
imageSrc={item.imageSrc}
title={item.title}
description={item.description}
actions={
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed={item.installed}
/>
}
/>
))}
</List>
@ -215,7 +235,7 @@ function Web3Container() {
<List>
<ListItem className={classNames("flex-col")}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3")}>
<Image width={40} height={40} src="/integrations/metamask.svg" alt="Embed" />
<Image width={40} height={40} src="/apps/metamask.svg" alt="Embed" />
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">
MetaMask (
@ -286,7 +306,7 @@ export default function IntegrationsPage() {
const { t } = useLocale();
return (
<Shell heading={t("integrations")} subtitle={t("connect_your_favourite_apps")}>
<Shell heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
<ClientSuspense fallback={<Loader />}>
<IntegrationsContainer />
<CalendarListContainer />

View File

@ -0,0 +1,118 @@
import showToast from "@calcom/lib/notification";
import App from "@components/App";
export default function NukeMyCal() {
return (
<App
name="Wipe my Cal"
type="wipemycal_other"
logo="/apps/nuke-my-cal.svg"
categories={["fun", "productivity"]}
author="/peer"
feeType="free" // "usage-based", "monthly", "one-time" or "free"
price={0} // 0 = free. if type="usage-based" it's the price per booking
commission={0} // only required for "usage-based" billing. % of commission for paid bookings
website="https://cal.com"
email="help@cal.com"
tos="https://cal.com/terms"
privacy="https://cal.com/privacy"
body={
<>
<style jsx>
{`
.pushable {
position: relative;
border: none;
background: transparent;
padding: 0;
cursor: pointer;
outline-offset: 4px;
transition: filter 250ms;
}
.shadow {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 12px;
background: hsl(0deg 0% 0% / 0.25);
will-change: transform;
transform: translateY(2px);
transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1);
}
.edge {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 12px;
background: linear-gradient(
to left,
hsl(340deg 100% 16%) 0%,
hsl(340deg 100% 32%) 8%,
hsl(340deg 100% 32%) 92%,
hsl(340deg 100% 16%) 100%
);
}
.front {
display: block;
position: relative;
padding: 12px 42px;
border-radius: 12px;
font-size: 1.25rem;
color: white;
background: hsl(345deg 100% 47%);
will-change: transform;
transform: translateY(-4px);
transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1);
}
.pushable:hover {
filter: brightness(110%);
}
.pushable:hover .front {
transform: translateY(-6px);
transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5);
}
.pushable:active .front {
transform: translateY(-2px);
transition: transform 34ms;
}
.pushable:hover .shadow {
transform: translateY(4px);
transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5);
}
.pushable:active .shadow {
transform: translateY(1px);
transition: transform 34ms;
}
.pushable:focus:not(:focus-visible) {
outline: none;
}
`}
</style>
<div>
Have an emergency? Need to reschedule all of your upcoming calendar events? Just click{" "}
<strong>Nuke my Cal</strong> and auto-reschedule the entire day. Give it a try!
<br />
<br />
Demo: <br />
<br />
</div>
<button
onClick={() => (
new Audio("/apps/nuke-my-cal.wav").play(),
showToast("All of your calendar events for today have been rescheduled", "success")
)}
className="pushable">
<span className="shadow"></span>
<span className="edge"></span>
<span className="front">Nuke my Cal</span>
</button>
</>
}
/>
);
}

View File

@ -13,8 +13,7 @@ import {
UsersIcon,
} from "@heroicons/react/solid";
import { zodResolver } from "@hookform/resolvers/zod";
import { MembershipRole } from "@prisma/client";
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { EventTypeCustomInput, MembershipRole, PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import * as RadioGroup from "@radix-ui/react-radio-group";
import classNames from "classnames";
@ -30,6 +29,7 @@ import Select, { Props as SelectProps } from "react-select";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
import getApps, { getLocationOptions, hasIntegration } from "@calcom/app-store/utils";
import showToast from "@calcom/lib/notification";
import { StripeData } from "@calcom/stripe/server";
import Button from "@calcom/ui/Button";
@ -42,7 +42,6 @@ import { asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { useLocale } from "@lib/hooks/useLocale";
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
import { LocationType } from "@lib/location";
import prisma from "@lib/prisma";
import { slugify } from "@lib/slugify";
@ -346,6 +345,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
);
case LocationType.Phone:
return <p className="text-sm">{t("cal_invitee_phone_number_scheduling")}</p>;
/* TODO: Render this dynamically from App Store */
case LocationType.GoogleMeet:
return <p className="text-sm">{t("cal_provide_google_meet_location")}</p>;
case LocationType.Zoom:
@ -358,6 +358,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
case LocationType.Tandem:
return <p className="text-sm">{t("cal_provide_tandem_meeting_url")}</p>;
case LocationType.Teams:
return <p className="text-sm">{t("cal_provide_teams_meeting_url")}</p>;
default:
return null;
}
@ -701,6 +703,78 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<span className="ml-2 text-sm">Jitsi Meet</span>
</div>
)}
{location.type === LocationType.Teams && (
<div className="flex flex-grow items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
viewBox="0 0 2228.833 2073.333">
<path
fill="#5059C9"
d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"
/>
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25" />
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917" />
<path
fill="#7B83EB"
d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"
/>
<path
opacity=".1"
d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"
/>
<path
opacity=".2"
d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
/>
<path
opacity=".2"
d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
/>
<path
opacity=".2"
d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"
/>
<path
opacity=".1"
d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"
/>
<path
opacity=".2"
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
/>
<path
opacity=".2"
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
/>
<path
opacity=".2"
d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"
/>
<linearGradient
id="a"
gradientUnits="userSpaceOnUse"
x1="198.099"
y1="1683.0726"
x2="942.2344"
y2="394.2607"
gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
<stop offset="0" stopColor="#5a62c3" />
<stop offset=".5" stopColor="#4d55bd" />
<stop offset="1" stopColor="#3940ab" />
</linearGradient>
<path
fill="url(#a)"
d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"
/>
<path
fill="#FFF"
d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"
/>
</svg>
<span className="ml-2 text-sm">MS Teams</span>
</div>
)}
<div className="flex">
<button
type="button"
@ -725,7 +799,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<li>
<button
type="button"
className="flex rounded-sm py-2 hover:bg-gray-100"
className="flex rounded-sm py-2 hover:bg-gray-100"
onClick={() => setShowLocationModal(true)}>
<PlusIcon className="mt-0.5 h-4 w-4 text-neutral-900" />
<span className="ml-1 text-sm font-medium text-neutral-700">{t("add_location")}</span>
@ -1529,7 +1603,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</Form>
</div>
</div>
<div className="m-0 mb-4 mt-0 w-full lg:w-3/12 lg:px-2 lg:ltr:ml-2 lg:rtl:mr-2">
<div className="m-0 mt-0 mb-4 w-full lg:w-3/12 lg:px-2 lg:ltr:ml-2 lg:rtl:mr-2">
<div className="px-2">
<Controller
name="hidden"
@ -1845,6 +1919,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
id: true,
type: true,
key: true,
userId: true,
},
});
@ -1872,17 +1947,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
eventType.users.push(fallbackUser);
}
const integrations = getIntegrations(credentials);
const integrations = getApps(credentials);
const locationOptions = getLocationOptions(integrations);
const locationOptions: OptionTypeBase[] = [];
if (hasIntegration(integrations, "zoom_video")) {
locationOptions.push({
value: LocationType.Zoom,
label: "Zoom Video",
disabled: true,
});
}
const hasPaymentIntegration = hasIntegration(integrations, "stripe_payment");
if (hasIntegration(integrations, "google_calendar")) {
locationOptions.push({
@ -1890,27 +1957,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
label: "Google Meet",
});
}
if (hasIntegration(integrations, "daily_video")) {
locationOptions.push({
value: LocationType.Daily,
label: "Daily.co Video",
});
}
if (hasIntegration(integrations, "jitsi_video")) {
locationOptions.push({
value: LocationType.Jitsi,
label: "Jitsi Meet",
});
}
if (hasIntegration(integrations, "huddle01_video")) {
locationOptions.push({
value: LocationType.Huddle01,
label: "Huddle01 Video",
});
}
if (hasIntegration(integrations, "tandem_video")) {
locationOptions.push({ value: LocationType.Tandem, label: "Tandem Video" });
}
const currency =
(credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData)
?.default_currency || "usd";

View File

@ -17,6 +17,8 @@ import { useForm } from "react-hook-form";
import TimezoneSelect from "react-timezone-select";
import * as z from "zod";
import getApps from "@calcom/app-store/utils";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Form } from "@calcom/ui/form/fields";
@ -25,8 +27,6 @@ import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { DEFAULT_SCHEDULE } from "@lib/availability";
import { useLocale } from "@lib/hooks/useLocale";
import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations/calendar/CalendarManager";
import getIntegrations from "@lib/integrations/getIntegrations";
import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
@ -676,10 +676,11 @@ export async function getServerSideProps(context: NextPageContext) {
id: true,
type: true,
key: true,
userId: true,
},
});
const integrations = getIntegrations(credentials)
const integrations = getApps(credentials)
.filter((item) => item.type.endsWith("_calendar"))
.map((item) => omit(item, "key"));

View File

@ -1,9 +0,0 @@
function RedirectPage() {
return null;
}
export async function getServerSideProps() {
return { redirect: { permanent: false, destination: "/integrations" } };
}
export default RedirectPage;

View File

@ -1,8 +1,15 @@
import { expect, test } from "@playwright/test";
import { expect, Page, test } from "@playwright/test";
import { deleteAllBookingsByEmail } from "./lib/teardown";
import { selectFirstAvailableTimeSlotNextMonth, todo } from "./lib/testUtils";
const bookTimeSlot = async (page: Page) => {
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
};
test.describe("free user", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/free");
@ -34,15 +41,8 @@ test.describe("free user", () => {
// save booking url
const bookingUrl: string = page.url();
const bookTimeSlot = async () => {
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
};
// book same time spot twice
await bookTimeSlot();
await bookTimeSlot(page);
// Make sure we're navigated to the success page
await page.waitForNavigation({
@ -55,7 +55,7 @@ test.describe("free user", () => {
await page.goto(bookingUrl);
// book same time spot again
await bookTimeSlot();
await bookTimeSlot(page);
// check for error message
await expect(page.locator("[data-testid=booking-fail]")).toBeVisible();
@ -86,10 +86,7 @@ test.describe("pro user", () => {
// Click first event type
await page.click('[data-testid="event-type-link"]');
await selectFirstAvailableTimeSlotNextMonth(page);
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
await bookTimeSlot(page);
// Make sure we're navigated to the success page
await page.waitForNavigation({

View File

@ -1,21 +1,26 @@
import { expect, test } from "@playwright/test";
import { hasIntegrationInstalled } from "../lib/integrations/getIntegrations";
import * as teardown from "./lib/teardown";
import { selectFirstAvailableTimeSlotNextMonth, todo } from "./lib/testUtils";
const IS_STRIPE_ENABLED = !!(
process.env.STRIPE_CLIENT_ID &&
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
process.env.STRIPE_PRIVATE_KEY
);
test.describe.serial("Stripe integration", () => {
test.afterAll(() => {
teardown.deleteAllPaymentsByEmail("pro@example.com");
teardown.deleteAllBookingsByEmail("pro@example.com");
});
test.skip(!hasIntegrationInstalled("stripe_payment"), "It should only run if Stripe is installed");
test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed");
test.describe.serial("Stripe integration dashboard", () => {
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
test("Can add Stripe integration", async ({ page }) => {
await page.goto("/integrations");
await page.goto("/apps/installed");
/** We should see the "Connect" button for Stripe */
await expect(
page.locator(`li:has-text("Stripe") >> [data-testid="integration-connection-button"]`)
@ -28,7 +33,7 @@ test.describe.serial("Stripe integration", () => {
]);
await Promise.all([
page.waitForNavigation({ url: "/integrations" }),
page.waitForNavigation({ url: "/apps/installed" }),
/** We skip filling Stripe forms (testing mode only) */
page.click('[id="skip-account-app"]'),
]);

View File

@ -12,7 +12,7 @@ test.describe("integrations", () => {
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
test.beforeEach(async ({ page }) => {
await page.goto("/integrations");
await page.goto("/apps/installed");
});
todo("Can add Zoom integration");

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2228.833 2073.333">
<path fill="#5059C9" d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"/>
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25"/>
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917"/>
<path fill="#7B83EB" d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"/>
<path opacity=".1" d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"/>
<path opacity=".2" d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
<path opacity=".2" d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
<path opacity=".2" d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"/>
<path opacity=".1" d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"/>
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
<path opacity=".2" d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"/>
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="198.099" y1="1683.0726" x2="942.2344" y2="394.2607" gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
<stop offset="0" stop-color="#5a62c3"/>
<stop offset=".5" stop-color="#4d55bd"/>
<stop offset="1" stop-color="#3940ab"/>
</linearGradient>
<path fill="url(#a)" d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"/>
<path fill="#FFF" d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" version="1.1" width="720" height="720" id="svg2">
<defs id="defs4">
<inkscape:path-effect effect="spiro" id="path-effect3201"/>
<inkscape:path-effect effect="spiro" id="path-effect3197"/>
<inkscape:path-effect effect="spiro" id="path-effect3193"/>
<inkscape:path-effect effect="spiro" id="path-effect3189"/>
<inkscape:path-effect effect="spiro" id="path-effect3185"/>
<inkscape:path-effect effect="spiro" id="path-effect3181"/>
<inkscape:path-effect effect="spiro" id="path-effect3177"/>
<inkscape:path-effect effect="spiro" id="path-effect3173"/>
<inkscape:path-effect effect="spiro" id="path-effect3169"/>
<inkscape:path-effect effect="spiro" id="path-effect3165"/>
<inkscape:path-effect effect="spiro" id="path-effect3161"/>
<inkscape:path-effect effect="spiro" id="path-effect3157"/>
<inkscape:path-effect effect="spiro" id="path-effect3153"/>
<inkscape:path-effect effect="spiro" id="path-effect3149"/>
<inkscape:path-effect effect="spiro" id="path-effect3149-6"/>
<inkscape:path-effect effect="spiro" id="path-effect3153-1"/>
<inkscape:path-effect effect="spiro" id="path-effect3157-8"/>
<inkscape:path-effect effect="spiro" id="path-effect3161-4"/>
<inkscape:path-effect effect="spiro" id="path-effect3165-8"/>
<inkscape:path-effect effect="spiro" id="path-effect3169-4"/>
<inkscape:path-effect effect="spiro" id="path-effect3173-0"/>
<inkscape:path-effect effect="spiro" id="path-effect3177-7"/>
<inkscape:path-effect effect="spiro" id="path-effect3181-6"/>
<inkscape:path-effect effect="spiro" id="path-effect3185-8"/>
<inkscape:path-effect effect="spiro" id="path-effect3189-9"/>
<inkscape:path-effect effect="spiro" id="path-effect3193-7"/>
<inkscape:path-effect effect="spiro" id="path-effect3197-3"/>
<inkscape:path-effect effect="spiro" id="path-effect3201-3"/>
</defs>
<path d="m 329.6885371631169,144.9454094414912 c 0,0 -9.3691648630806,-18.5615530306314 -37.6534361478519,-6.3639610390736" id="path2983" style="fill:none;stroke:#ff0000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:none"/>
<path d="m 326.8905349710195,144.9454094414912 c 0,0 -9.3691648630806,-18.5615530306314 -37.6534361478519,-6.3639610390736" id="path2983-8" style="fill:none;stroke:#ff0000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:none"/>
<path d="M 94.6324614631855,36.00250586938022 300.1705854663462,193.5309023590188 342.0166705927379,44.6173400524073 401.0934966535268,195.9922835541695 564.785535530295,54.46286483300981 476.1702964391128,237.8357638717294 616.4777583334849,232.9130014814282 525.4009848231031,347.3672270559314 647.2469385734796,426.1314253007506 502.0164078407082,428.5928064959011 630.0161976390828,582.4291311928135 446.6318834087184,497.5114799601184 426.9396080551219,662.4240200352077 337.0936017543393,529.5094354970756 211.5553463751634,700.5754285600419 223.863018471161,486.4352645819405 52.7863763367938,520.8946013140482 159.8631235719732,380.5958731904649 72.47865169038992,237.8357638717299 194.3246054407665,261.2188852256607 94.6324614631855,36.00250586938069 z" id="path3508" style="fill:#ff0000;stroke:none"/>
<path d="m 160.1985233083172,118.7982717399246 154.3242216264676,118.2770704473064 31.419302606586,-111.8087931572194 44.3566625034159,113.6568723829586 122.9049190198811,-106.2645554800019 -66.5349937551232,137.6819023175674 105.347073445612,-3.6961584514783 -68.3831880260988,85.9356839968712 91.4856164132948,59.1385352236531 -109.0434619875631,1.8480792257391 96.1061020907337,115.5049516086972 -137.6904731876862,-63.7587332880004 -14.7855541678056,123.8213081245232 -67.4590908906114,-99.7962781899145 -94.2579078197586,128.4415061888714 9.2409713548783,-160.7828926393062 -128.4495018328083,25.8731091603477 80.3964507874411,-105.3405158671315 -65.6108966196359,-107.1885950928715 91.4856164132951,17.5567526445221 -74.8518679745143,-169.0992491551333 z" id="path3512" style="fill:#ff8000;stroke:none"/>
<path d="m 226.2365903979516,204.2096633100054 99.1022747159463,75.9539014973134 20.1765110200129,-71.8001725091792 28.4844861459008,72.9869522200746 78.9257636959331,-68.2398333764925 -42.7267292188508,88.4150884617163 67.6506545965139,-2.373559421791 -43.9135828082631,55.1852565566419 58.7492526759201,37.9769507486567 -70.0243617753388,1.1867797108955 61.7163866494512,74.17373193097 -88.4205924112332,-40.9439000258953 -9.4948287153004,79.5142406299997 -43.3201560135574,-64.0861043883581 -60.529533060039,82.4811899072385 5.9342679470626,-103.2498348479099 -82.4863244641708,16.614915952537 51.6281311394449,-67.6464435210443 -42.1333024241446,-68.8332232319403 58.7492526759201,11.2744072535075 -48.0675703712075,-108.5903435469403 z" id="path3514" style="fill:#ffff00;stroke:none"/>
<path d="m -57.96064186096192,305.2593994140625 a 30.91234397888183,30.91234397888183 0 1 1 -61.82468795776368,0 30.91234397888183,30.91234397888183 0 1 1 61.82468795776368,0 z" transform="translate(435.3488372093023,54.09660107334526)" id="path3516" style="fill:#000000;fill-opacity:1;stroke:none"/>
<metadata>
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/>
<dc:publisher>
<cc:Agent rdf:about="http://openclipart.org/">
<dc:title>Open Clip Art Library</dc:title>
</cc:Agent>
</dc:publisher>
<dc:title>one eyed sun</dc:title>
<dc:date>2011-07-10T22:19:30</dc:date>
<dc:description>my mom came up with the name for this!</dc:description>
<dc:source>http://openclipart.org/detail/148927/one-eyed-sun-by-10binary</dc:source>
<dc:creator>
<cc:Agent>
<dc:title>10binary</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>clip art</rdf:li>
<rdf:li>clipart</rdf:li>
<rdf:li>eyed</rdf:li>
<rdf:li>one</rdf:li>
<rdf:li>sun</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
</cc:License>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 509 B

After

Width:  |  Height:  |  Size: 509 B

View File

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 981 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 29.2 33" style="enable-background:new 0 0 29.2 33;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F68D2E;}
</style>
<path class="st0" d="M27.2,18.5c-1.5-0.9-3.3-0.8-4.7,0.2c-0.3,0.2-0.6,0.2-0.9,0l-4.2-2.4c-0.1,0-0.1-0.1,0-0.1c0,0,0,0,0,0
l4.2-2.4c0.3-0.2,0.7-0.1,0.9,0.1c1.4,1,3.2,1.1,4.7,0.2c1.9-1.2,2.6-3.7,1.5-5.7c-1.1-2.1-3.7-2.8-5.8-1.7
c-1.5,0.8-2.4,2.4-2.2,4.1c0,0.3-0.1,0.7-0.4,0.8L16,13.9c-0.1,0-0.1,0-0.1,0c0,0,0,0,0-0.1V8.9c0-0.3,0.2-0.6,0.5-0.8
c2.1-1,3.1-3.5,2.1-5.7S15-0.6,12.8,0.4S9.7,3.9,10.7,6c0.4,0.9,1.2,1.7,2.1,2.1c0.3,0.1,0.5,0.4,0.5,0.8v4.9c0,0.1,0,0.1-0.1,0.1
c0,0,0,0-0.1,0l-4.2-2.4c-0.3-0.2-0.5-0.5-0.4-0.8C8.7,8.3,7,6.2,4.6,6S0.2,7.6,0,9.9s1.5,4.4,3.9,4.6c1,0.1,2-0.2,2.9-0.8
c0.3-0.2,0.6-0.2,0.9-0.1l4.2,2.4c0.1,0,0.1,0.1,0,0.1c0,0,0,0,0,0l-4.2,2.4c-0.3,0.2-0.7,0.1-0.9,0c-1.4-1-3.2-1-4.7-0.2
c-1.9,1.2-2.6,3.7-1.5,5.7c1.1,2.1,3.7,2.8,5.8,1.7c1.5-0.8,2.4-2.4,2.2-4.1c0-0.3,0.1-0.7,0.4-0.8l4.2-2.4c0.1,0,0.1,0,0.1,0
c0,0,0,0,0,0.1v4.9c0,0.3-0.2,0.6-0.5,0.8c-2.1,1-3.1,3.5-2.1,5.7s3.5,3.1,5.7,2.1c2.1-1,3.1-3.5,2.1-5.7c-0.4-0.9-1.2-1.7-2.1-2.1
c-0.3-0.1-0.5-0.4-0.5-0.8v-4.9c0-0.1,0-0.1,0.1-0.1c0,0,0,0,0.1,0l4.2,2.4c0.3,0.2,0.5,0.5,0.4,0.8c-0.2,2.3,1.5,4.4,3.9,4.6
c1.7,0.2,3.3-0.7,4.1-2.2C29.8,22.2,29.1,19.7,27.2,18.5L27.2,18.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -583,6 +583,7 @@
"cal_provide_video_meeting_url": "Cal will provide a Daily video meeting URL.",
"cal_provide_jitsi_meeting_url": "We will generate a Jitsi Meet URL for you.",
"cal_provide_huddle01_meeting_url": "Cal will provide a Huddle01 web3 video meeting URL.",
"cal_provide_teams_meeting_url": "Cal will provide a MS Teams meeting URL. NOTE: MUST HAVE A WORK OR SCHOOL ACCOUNT",
"require_payment": "Require Payment",
"commission_per_transaction": "commission per transaction",
"event_type_updated_successfully_description": "Your event type has been updated successfully.",
@ -608,6 +609,9 @@
"confirm_delete_account": "Yes, delete account",
"delete_account_confirmation_message": "Are you sure you want to delete your Cal.com account? Anyone who you've shared your account link with will no longer be able to book using it and any preferences you have saved will be lost.",
"integrations": "Integrations",
"apps": "Apps",
"app_store": "App Store",
"app_store_description": "Connecting people, technology and the workplace.",
"settings": "Settings",
"event_type_moved_successfully": "Event type has been moved successfully",
"next_step": "Skip step",
@ -655,6 +659,24 @@
"import_from": "Import from",
"access_token": "Access token",
"visit_roadmap": "Roadmap",
"popular_categories": "Popular Categories",
"trending_apps": "Trending Apps",
"all_apps": "All Apps",
"installed_apps": "Installed Apps",
"manage_your_connected_apps": "Manage your connected apps",
"browse_apps": "Browse Apps",
"features": "Features",
"permissions": "Permissions",
"terms_and_privacy": "Terms and Privacy",
"published_by": "Published by {{author}}",
"subscribe": "Subscribe",
"buy": "Buy",
"install_app": "Install App",
"categories": "Categories",
"pricing": "Pricing",
"learn_more": "Learn more",
"privacy_policy": "Privacy Policy",
"terms_of_service": "Terms of Service",
"remove": "Remove",
"add": "Add",
"verify_wallet": "Verify Wallet",

View File

@ -54,6 +54,7 @@ async function getUserFromSession({
id: true,
type: true,
key: true,
userId: true,
},
orderBy: {
id: "asc",

View File

@ -3,11 +3,11 @@ import _ from "lodash";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
import getApps from "@calcom/app-store/utils";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { checkPremiumUsername } from "@calcom/ee/lib/core/checkPremiumUsername";
import { checkRegularUsername } from "@lib/core/checkRegularUsername";
import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations/calendar/CalendarManager";
import { ALL_INTEGRATIONS } from "@lib/integrations/getIntegrations";
import jackson from "@lib/jackson";
import {
isSAMLLoginEnabled,
@ -519,16 +519,16 @@ const loggedInViewerRouter = createProtectedRouter()
function countActive(items: { credentialIds: unknown[] }[]) {
return items.reduce((acc, item) => acc + item.credentialIds.length, 0);
}
const integrations = ALL_INTEGRATIONS.map((integration) => ({
...integration,
credentialIds: credentials
.filter((credential) => credential.type === integration.type)
.map((credential) => credential.id),
}));
const apps = getApps(credentials).map(
({ credentials: _, credential: _1 /* don't leak to frontend */, ...app }) => ({
...app,
credentialIds: credentials.filter((c) => c.type === app.type).map((c) => c.id),
})
);
// `flatMap()` these work like `.filter()` but infers the types correctly
const conferencing = integrations.flatMap((item) => (item.variant === "conferencing" ? [item] : []));
const payment = integrations.flatMap((item) => (item.variant === "payment" ? [item] : []));
const calendar = integrations.flatMap((item) => (item.variant === "calendar" ? [item] : []));
const conferencing = apps.flatMap((item) => (item.variant === "conferencing" ? [item] : []));
const payment = apps.flatMap((item) => (item.variant === "payment" ? [item] : []));
const calendar = apps.flatMap((item) => (item.variant === "calendar" ? [item] : []));
return {
conferencing: {

View File

@ -9,9 +9,15 @@
"@ee/*": ["ee/*"],
"@prisma/client/*": ["@calcom/prisma/client/*"]
},
"typeRoots": ["./types"],
"typeRoots": ["./types", "@calcom/types"],
"types": ["@types/jest"]
},
"include": ["next-env.d.ts", "@types/*.d.ts", "**/*.ts", "**/*.tsx"],
"include": [
"next-env.d.ts",
"../../packages/types/*.d.ts",
"../../packages/types/next-auth.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff

@ -1 +1 @@
Subproject commit e54a7cc0ecbb36a5a6838f77d8c19ec008c8849a
Subproject commit d9c37b322081604f5dee0ca471cb0f5d895af8bc

View File

@ -4,7 +4,8 @@
"private": true,
"workspaces": [
"apps/*",
"packages/*"
"packages/*",
"packages/app-store/*"
],
"scripts": {
"build": "turbo run build --scope=\"@calcom/web\" --include-dependencies",

View File

@ -0,0 +1,12 @@
import { App } from "@calcom/types/App";
import appStore from ".";
/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
export function getAppRegistry() {
return Object.values(appStore).map((app) => {
// Let's not leak api keys to the front end
const { key, ...metadata } = app.metadata;
return metadata;
}) as App[];
}

View File

@ -0,0 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
/**
* This is an example endoint for an app, these will run under `/api/integrations/[...args]`
* @param req
* @param res
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200);
}

View File

@ -0,0 +1 @@
export { default as example } from "./example";

View File

@ -0,0 +1,13 @@
import { InstallAppButtonProps } from "../../types";
export default function InstallAppButton(props: InstallAppButtonProps) {
return (
<>
{props.render({
onClick() {
alert("You can put your install code in here!");
},
})}
</>
);
}

View File

@ -0,0 +1 @@
export { default as InstallAppButton } from "./InstallAppButton";

Some files were not shown because too many files have changed in this diff Show More