8931-cal-1701-change-ee-to-commercial
Peer Richelsen 2023-04-27 12:07:02 +01:00
commit 807f186f6b
389 changed files with 11595 additions and 8002 deletions

View File

@ -1,42 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-outside-of-docker
{
"name": "Docker outside of Docker",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-from-docker:1": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers-contrib/features/npm-package:1": {},
"ghcr.io/devcontainers-contrib/features/jest:2": {},
"ghcr.io/devcontainers-contrib/features/prisma:2": {},
"ghcr.io/guiyomh/features/vim:0": {}
},
// Use this environment variable if you need to bind mount your local source code into a new container.
"remoteEnv": {
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
},
"hostRequirements": {
"cpus": 4,
"memory": "8gb"
},
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "./deploy/install.sh"
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View File

@ -6,6 +6,6 @@
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown", "typescript"],
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
}

View File

@ -71,5 +71,5 @@ module.exports = {
return config;
},
typescript: { reactDocgen: 'react-docgen' }
typescript: { reactDocgen: "react-docgen" },
};

View File

@ -11,7 +11,7 @@ import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
import classNames from "@lib/classNames";

View File

@ -280,13 +280,13 @@ function BookingListItem(booking: BookingItemProps) {
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
/>
{booking.paid && (
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
setIsOpenDialog={setChargeCardDialogIsOpen}
bookingId={booking.id}
paymentAmount={booking?.payment[0].amount}
paymentCurrency={booking?.payment[0].currency}
paymentAmount={booking.payment[0].amount}
paymentCurrency={booking.payment[0].currency}
/>
)}
{showRecordingsButtons && (
@ -354,11 +354,15 @@ function BookingListItem(booking: BookingItemProps) {
{booking.eventType.team.name}
</Badge>
)}
{booking.paid && (
{booking.paid && !booking.payment[0] ? (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("error_collecting_card")}
</Badge>
) : booking.paid ? (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
</Badge>
)}
) : null}
{recurringDates !== undefined && (
<div className="text-muted mt-2 text-sm">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
@ -458,7 +462,7 @@ function BookingListItem(booking: BookingItemProps) {
<RequestSentMessage />
</div>
)}
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
{booking.status === "ACCEPTED" && booking.paid && booking.payment[0]?.paymentOption === "HOLD" && (
<div className="ml-2">
<TableActions actions={chargeCardActions} />
</div>
@ -483,12 +487,12 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
i18n: { language },
} = useLocale();
const now = new Date();
const recurringCount = recurringDates.filter((date) => {
const recurringCount = recurringDates.filter((recurringDate) => {
return (
date >= now &&
recurringDate >= now &&
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
.map((date) => date.toDateString())
.includes(date.toDateString())
.includes(recurringDate.toDateString())
);
}).length;

View File

@ -19,6 +19,8 @@ import { MetaProvider } from "@calcom/ui";
import usePublicPage from "@lib/hooks/usePublicPage";
import type { WithNonceProps } from "@lib/withNonce";
import { useViewerI18n } from "@components/I18nLanguageHandler";
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
({ children }) => <>{children}</>
);
@ -46,9 +48,7 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
**/
const { i18n, locale } = trpc.viewer.public.i18n.useQuery(undefined, {
trpc: { context: { skipBatch: true } },
}).data ?? {
const { i18n, locale } = useViewerI18n().data ?? {
locale: "en",
};

View File

@ -7,7 +7,7 @@ import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
enum DirectAction {
ACCEPT = "accept",
@ -51,7 +51,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
try {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res }, sessionGetter);
const caller = viewerRouter.createCaller(ctx);
const caller = viewerRouter.createCaller({ ...ctx, req, res });
await caller.bookings.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,

View File

@ -6,6 +6,7 @@ import path from "path";
import { z } from "zod";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@ -108,9 +109,11 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
if (data.items) {
data.items = data.items.map((item) => {
if (typeof item === "string" && !item.includes("/api/app-store")) {
// Make relative paths absolute
return `/api/app-store/${appDirname}/${item}`;
if (typeof item === "string") {
return getAppAssetFullPath(item, {
dirName: singleApp.dirName,
isTemplate: singleApp.isTemplate,
});
}
return item;
});

View File

@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
import { getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
import { HeadSeo } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -25,7 +26,13 @@ export default function SetupInformation(props: InferGetStaticPropsType<typeof g
});
}
return <AppSetupPage slug={slug} {...props} />;
return (
<>
{/* So that the set up page does not get indexed by search engines */}
<HeadSeo nextSeoProps={{ noindex: true, nofollow: true }} title={`${slug} | Cal.com`} description="" />
<AppSetupPage slug={slug} {...props} />
</>
);
}
SetupInformation.PageWrapper = PageWrapper;

View File

@ -46,10 +46,10 @@ const TwoFactorAuthView = () => {
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
}
/>
<div>
<div className="!mx-4">
<div className="flex">
<p className="text-default font-semibold">{t("two_factor_auth")}</p>
<Badge className="ml-2 text-xs" variant={user?.twoFactorEnabled ? "success" : "gray"}>
<Badge className="mx-2 text-xs" variant={user?.twoFactorEnabled ? "success" : "gray"}>
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
</Badge>
</div>

View File

@ -263,7 +263,7 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
);
}
VideoMeetingInfo.PageWrapper = PageWrapper;
JoinCall.PageWrapper = PageWrapper;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req, res } = context;

View File

@ -1799,5 +1799,6 @@
"charge_attendee": "Charge attendee {{amount, currency}}",
"payment_app_commission": "Require payment ({{paymentFeePercentage}}% + {{fee, currency}} commission per transaction)",
"email_invite_team": "{{email}} has been invited",
"error_collecting_card": "Error collecting card",
"image_size_limit_exceed": "Uploaded image shouldn't exceed 5mb size limit"
}

View File

@ -465,6 +465,7 @@
"friday": "Vendredi",
"saturday": "Samedi",
"sunday": "Dimanche",
"all_booked_today": "Tout est réservé.",
"slots_load_fail": "Impossible de charger les créneaux disponibles.",
"additional_guests": "Ajouter des invités",
"your_name": "Votre nom",
@ -1792,10 +1793,12 @@
"seats_and_no_show_fee_error": "Il n'est pas possible d'activer les places et de facturer des frais d'absence pour le moment",
"complete_your_booking": "Terminer votre réservation",
"complete_your_booking_subject": "Terminer votre réservation : {{title}} le {{date}}",
"confirm_your_details": "Confirmez vos coordonnées",
"currency_string": "{{amount, currency}}",
"charge_card_dialog_body": "Vous êtes sur le point de facturer {{amount, currency}} au participant. Voulez-vous vraiment continuer ?",
"charge_attendee": "Facturer {{amount, currency}} au participant",
"payment_app_commission": "Exiger un paiement ({{paymentFeePercentage}} % + {{fee, currency}} de commission par transaction)",
"email_invite_team": "{{email}} a été invité",
"error_collecting_card": "Erreur lors de la collecte de la carte",
"image_size_limit_exceed": "L'image téléchargée ne doit pas dépasser 5 Mo"
}

View File

@ -15,15 +15,14 @@ import { v4 as uuidv4 } from "uuid";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { BookingStatus } from "@calcom/prisma/client";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { getSchedule } from "@calcom/trpc/server/routers/viewer/slots";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
import { prismaMock, CalendarManagerMock } from "../../../../tests/config/singleton";
// TODO: Mock properly
prismaMock.eventType.findUnique.mockResolvedValue(null);
prismaMock.user.findMany.mockResolvedValue([]);
prismaMock.selectedSlots.findMany.mockResolvedValue([]);
jest.mock("@calcom/lib/constants", () => ({
IS_PRODUCTION: true,
@ -271,16 +270,13 @@ describe("getSchedule", () => {
end: `${plus2DateString}T23:00:00.000Z`,
},
]);
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], {
@ -357,17 +353,14 @@ describe("getSchedule", () => {
});
// Day Plus 2 is completely free - It only has non accepted bookings
const scheduleOnCompletelyFreeDay = await getSchedule(
{
eventTypeId: 1,
// EventTypeSlug doesn't matter for non-dynamic events
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleOnCompletelyFreeDay = await getSchedule({
eventTypeId: 1,
// EventTypeSlug doesn't matter for non-dynamic events
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// getSchedule returns timeslots in GMT
expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots(
@ -390,16 +383,13 @@ describe("getSchedule", () => {
);
// Day plus 3
const scheduleForDayWithOneBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForDayWithOneBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
[
@ -455,16 +445,13 @@ describe("getSchedule", () => {
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const scheduleForEventWith30Length = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventWith30Length = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventWith30Length).toHaveTimeSlots(
[
`04:00:00.000Z`,
@ -490,16 +477,13 @@ describe("getSchedule", () => {
}
);
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule(
{
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// `slotInterval` takes precedence over `length`
expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots(
[`04:00:00.000Z`, `06:00:00.000Z`, `08:00:00.000Z`, `10:00:00.000Z`, `12:00:00.000Z`],
@ -553,16 +537,13 @@ describe("getSchedule", () => {
});
const { dateString: todayDateString } = getDate();
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
const scheduleForEventWithBookingNotice13Hrs = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventWithBookingNotice13Hrs = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
[
/*`04:00:00.000Z`, `06:00:00.000Z`, - Minimum time slot is 07:30 UTC*/ `08:00:00.000Z`,
@ -574,16 +555,13 @@ describe("getSchedule", () => {
}
);
const scheduleForEventWithBookingNotice10Hrs = await getSchedule(
{
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
[
/*`04:00:00.000Z`, - Minimum bookable time slot is 04:30 UTC but next available is 06:00*/
@ -639,16 +617,13 @@ describe("getSchedule", () => {
},
]);
const scheduleForEventOnADayWithNonCalBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventOnADayWithNonCalBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
[
@ -714,16 +689,13 @@ describe("getSchedule", () => {
},
]);
const scheduleForEventOnADayWithCalBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventOnADayWithCalBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
[
@ -767,16 +739,13 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData);
const scheduleForEventOnADayWithDateOverride = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForEventOnADayWithDateOverride = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"],
@ -853,16 +822,13 @@ describe("getSchedule", () => {
// Requesting this user's availability for their
// individual Event Type
const thisUserAvailability = await getSchedule(
{
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const thisUserAvailability = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(thisUserAvailability).toHaveTimeSlots(
[
@ -951,16 +917,13 @@ describe("getSchedule", () => {
hosts: [],
});
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${todayDateString}T18:30:00.000Z`,
endTime: `${plus1DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${todayDateString}T18:30:00.000Z`,
endTime: `${plus1DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
[
@ -981,16 +944,13 @@ describe("getSchedule", () => {
}
);
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// A user with blocked time in another event, still affects Team Event availability
// It's a collective availability, so both user 101 and 102 are considered for timeslots
expect(scheduleForTeamEventOnADayWithOneBookingForEachUser).toHaveTimeSlots(
@ -1088,16 +1048,13 @@ describe("getSchedule", () => {
],
hosts: [],
});
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
[
@ -1116,16 +1073,13 @@ describe("getSchedule", () => {
{ dateString: plus2DateString }
);
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule(
{
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
},
ctx
);
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
// A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
[

View File

@ -11,10 +11,6 @@ export async function getAppWithMetadata(app: { dirName: string }) {
// Let's not leak api keys to the front end
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { key, ...metadata } = appMetadata;
if (metadata.logo && !metadata.logo.includes("/api/app-store/")) {
const appDirName = `${metadata.isTemplate ? "templates" : ""}/${app.dirName}`;
metadata.logo = `/api/app-store/${appDirName}/${metadata.logo}`;
}
return metadata;
}

View File

@ -1,8 +1,8 @@
---
items:
- /api/app-store/amie/1.jpg
- /api/app-store/amie/2.jpg
- /api/app-store/amie/3.jpg
- 1.jpg
- 2.jpg
- 3.jpg
---
<iframe class="w-full aspect-video -mx-2" width="560" height="315" src="https://www.youtube.com/embed/OGe1NYKhZE8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

View File

@ -3,8 +3,7 @@
"name": "Amie",
"slug": "amie",
"type": "amie_other",
"imageSrc": "/api/app-store/amie/icon.svg",
"logo": "/api/app-store/amie/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/amie",
"variant": "other",
"categories": ["calendar"],

View File

@ -1,6 +1,7 @@
import type { AppMeta } from "@calcom/types/App";
import { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated";
import { getAppAssetFullPath } from "./getAppAssetFullPath";
type RawAppStoreMetaData = typeof rawAppStoreMetadata;
type AppStoreMetaData = {
@ -8,12 +9,19 @@ type AppStoreMetaData = {
};
export const appStoreMetadata = {} as AppStoreMetaData;
for (const [key, value] of Object.entries(rawAppStoreMetadata)) {
appStoreMetadata[key as keyof typeof appStoreMetadata] = {
const dirName = "dirName" in value ? value.dirName : value.slug;
if (!dirName) {
throw new Error(`Couldn't derive dirName for app ${key}`);
}
const metadata = (appStoreMetadata[key as keyof typeof appStoreMetadata] = {
appData: null,
dirName: "dirName" in value ? value.dirName : value.slug,
dirName,
__template: "",
...value,
} as AppStoreMetaData[keyof AppStoreMetaData];
} as AppStoreMetaData[keyof AppStoreMetaData]);
metadata.logo = getAppAssetFullPath(metadata.logo, {
dirName,
isTemplate: metadata.isTemplate,
});
}

View File

@ -1,6 +1,6 @@
---
items:
- /api/app-store/applecalendar/1.jpg
- 1.jpg
---
Apple calendar runs both the macOS and iOS mobile operating systems. Offering online cloud backup of calendars using Apples iCloud service, it can sync with Google Calendar and Microsoft Exchange Server. Users can schedule events in their day that include time, location, duration, and extra notes.

View File

@ -8,11 +8,10 @@ export const metadata = {
installed: true,
type: "apple_calendar",
title: "Apple Calendar",
imageSrc: "/api/app-store/applecalendar/icon.svg",
variant: "calendar",
categories: ["calendar"],
category: "calendar",
logo: "/api/app-store/applecalendar/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
slug: "apple-calendar",
url: "https://cal.com/",

View File

@ -1,13 +1,13 @@
---
items:
- /api/app-store/around/1.jpg
- /api/app-store/around/2.jpg
- /api/app-store/around/3.jpg
- /api/app-store/around/4.jpg
- /api/app-store/around/5.jpg
- /api/app-store/around/6.jpg
- /api/app-store/around/7.jpg
- /api/app-store/around/8.jpg
- 1.jpg
- 2.jpg
- 3.jpg
- 4.jpg
- 5.jpg
- 6.jpg
- 7.jpg
- 8.jpg
---
Discover radically unique video calls designed to help hybrid-remote teams create, collaborate and celebrate together.

View File

@ -4,7 +4,7 @@
"title": "Around",
"slug": "around",
"type": "around_video",
"logo": "/api/app-store/around/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/around",
"variant": "conferencing",
"categories": ["video"],

View File

@ -1,6 +1,6 @@
---
items:
- /api/app-store/caldavcalendar/1.jpg
- 1.jpg
---
Caldav is a protocol that allows different clients/servers to access scheduling information on remote servers as well as schedule meetings with other users on the same server or other servers. It extends WebDAV specification and uses iCalendar format for the data.

View File

@ -8,15 +8,15 @@ export const metadata = {
installed: true,
type: "caldav_calendar",
title: "CalDav (Beta)",
imageSrc: "/api/app-store/caldavcalendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/caldavcalendar/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
slug: "caldav-calendar",
url: "https://cal.com/",
email: "ali@cal.com",
dirName: "caldavcalendar",
} as AppMeta;
export default metadata;

View File

@ -8,11 +8,10 @@ export const metadata = {
installed: true,
type: "caldav_calendar",
title: "CalDav (Beta)",
imageSrc: "/api/app-store/caldavcalendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/caldavcalendar/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
slug: "caldav-calendar",
url: "https://cal.com/",

View File

@ -1,12 +1,12 @@
---
items:
- /api/app-store/campfire/1.jpg
- /api/app-store/campfire/2.jpg
- /api/app-store/campfire/3.jpg
- /api/app-store/campfire/4.jpg
- 1.jpg
- 2.jpg
- 3.jpg
- 4.jpg
---
<iframe class="w-full aspect-video -mx-2" width="560" height="315" src="https://player.vimeo.com/video/683733529?app_id=122963&h=025a2fae94&referrer=https%3A%2F%2Fwww.campfire.to%2F" />
<iframe class="w-full aspect-video -mx-2" width="560" height="315" src="https://player.vimeo.com/video/683733529?app_id=122963&h=025a2fae94&referrer=https%3A%2F%2Fwww.campfire.to%2F" ></iframe>
## Feel connected with your remote team

View File

@ -3,8 +3,7 @@
"name": "Campfire",
"slug": "campfire",
"type": "campfire_video",
"imageSrc": "/api/app-store/campfire/icon.svg",
"logo": "/api/app-store/campfire/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/campfire",
"variant": "conferencing",
"categories": ["video"],

View File

@ -1,10 +1,10 @@
---
items:
- /api/app-store/closecom/1.jpg
- /api/app-store/closecom/2.jpg
- /api/app-store/closecom/3.jpg
- /api/app-store/closecom/4.jpg
- /api/app-store/closecom/5.jpg
- 1.jpg
- 2.jpg
- 3.jpg
- 4.jpg
- 5.jpg
---
- Close is a modern CRM with build-in sales communication tools for email, phone, SMS, and meetings.

View File

@ -4,8 +4,7 @@
"title": "Close.com",
"slug": "closecom",
"type": "closecom_other_calendar",
"imageSrc": "/api/app-store/closecom/icon.svg",
"logo": "/api/app-store/closecom/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/closecom",
"variant": "other",
"categories": ["other"],

View File

@ -3,7 +3,6 @@
"name": "Cron",
"slug": "cron",
"type": "cron_other",
"imageSrc": "logo.png",
"logo": "logo.png",
"url": "https://cal.com/apps/cron",
"variant": "other",

View File

@ -1,8 +1,8 @@
---
items:
- /api/app-store/dailyvideo/1.jpg
- /api/app-store/dailyvideo/2.jpg
- /api/app-store/dailyvideo/3.jpg
- 1.jpg
- 2.jpg
- 3.jpg
---
- **Recordings require a team plan**

View File

@ -7,11 +7,10 @@ export const metadata = {
description: _package.description,
installed: !!process.env.DAILY_API_KEY,
type: "daily_video",
imageSrc: "/api/app-store/dailyvideo/icon.svg",
variant: "conferencing",
url: "https://daily.co",
categories: ["video"],
logo: "/api/app-store/dailyvideo/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
category: "video",
slug: "daily-video",

View File

@ -3,8 +3,7 @@
"name": "Discord",
"slug": "discord",
"type": "discord_video",
"imageSrc": "/api/app-store/discord/icon.svg",
"logo": "/api/app-store/discord/icon.svg",
"logo": "icon.svg",
"url": "https://discord.com/",
"variant": "conferencing",
"categories": ["video"],
@ -16,7 +15,7 @@
"label": "{TITLE}",
"linkType": "static",
"organizerInputPlaceholder": "https://discord.gg/420gg69",
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?discord.gg\\/[a-zA-Z0-9]*"
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?(discord.gg|discord.com)\\/[a-zA-Z0-9]*"
}
},
"description": "Copy your server invite link and start scheduling calls in Discord! Discord is a VoIP and instant messaging social platform. Users have the ability to communicate with voice calls, video calls, text messaging, media and files in private chats or as part of communities.",

View File

@ -8,12 +8,11 @@ export const metadata = {
installed: true,
type: "exchange2013_calendar",
title: "Microsoft Exchange 2013 Calendar",
imageSrc: "/api/app-store/exchange2013calendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
label: "Exchange Calendar",
logo: "/api/app-store/exchange2013calendar/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
slug: "exchange2013-calendar",
url: "https://cal.com/",

View File

@ -8,12 +8,11 @@ export const metadata = {
installed: true,
type: "exchange2016_calendar",
title: "Microsoft Exchange 2016 Calendar",
imageSrc: "/api/app-store/exchange2016calendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
label: "Exchange Calendar",
logo: "/api/app-store/exchange2016calendar/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
slug: "exchange2016-calendar",
url: "https://cal.com/",

View File

@ -3,9 +3,9 @@
"title": "Microsoft Exchange",
"name": "Microsoft Exchange",
"slug": "exchange",
"dirName": "exchangecalendar",
"type": "exchange_calendar",
"imageSrc": "/api/app-store/exchangecalendar/icon.svg",
"logo": "/api/app-store/exchangecalendar/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/exchange",
"variant": "calendar",
"categories": ["calendar"],

View File

@ -1,7 +1,7 @@
---
items:
- /api/app-store/facetime/facetime1.png
- /api/app-store/facetime/facetime2.png
- facetime1.png
- facetime2.png
---
With FaceTime, its easy to stay in touch. You can make audio and video calls with up to 32 people, share your screen, enjoy films and music together, and more.

View File

@ -4,8 +4,7 @@
"title": "Facetime",
"slug": "facetime",
"type": "facetime_video",
"imageSrc": "/api/app-store/facetime/icon.svg",
"logo": "/api/app-store/facetime/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/facetime",
"variant": "conferencing",
"categories": ["video"],

View File

@ -1,6 +1,6 @@
---
items:
- /api/app-store/fathom/1.jpg
- 1.jpg
---
Fathom Analytics provides simple, privacy-focused website analytics. We're a GDPR-compliant, Google Analytics alternative.

View File

@ -3,8 +3,7 @@
"name": "Fathom",
"slug": "fathom",
"type": "fathom_analytics",
"imageSrc": "/api/app-store/fathom/icon.svg",
"logo": "/api/app-store/fathom/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/fathom",
"variant": "analytics",
"categories": ["analytics"],

View File

@ -1,11 +1,11 @@
---
description: Google Analytics is a web analytics service offered by Google that tracks and reports website traffic, currently as a platform inside the Google Marketing Platform brand.
items:
- /api/app-store/ga4/1.jpeg
- /api/app-store/ga4/2.jpeg
- /api/app-store/ga4/3.jpeg
- /api/app-store/ga4/4.jpeg
- /api/app-store/ga4/5.jpeg
- 1.jpeg
- 2.jpeg
- 3.jpeg
- 4.jpeg
- 5.jpeg
---
Google Analytics is a web analytics service offered by Google that tracks and reports website traffic, currently as a platform inside the Google Marketing Platform brand.

View File

@ -3,8 +3,7 @@
"name": "Google Analytics",
"slug": "ga4",
"type": "ga4_analytics",
"imageSrc": "/api/app-store/ga4/icon.svg",
"logo": "/api/app-store/ga4/icon.svg",
"logo": "icon.svg",
"url": "https://marketingplatform.google.com",
"variant": "analytics",
"categories": ["analytics"],

View File

@ -0,0 +1,10 @@
import type { App } from "@calcom/types/App";
export function getAppAssetFullPath(assetPath: string, metadata: Pick<App, "dirName" | "isTemplate">) {
const appDirName = `${metadata.isTemplate ? "templates/" : ""}${metadata.dirName}`;
let assetFullPath = assetPath;
if (!assetPath.startsWith("/app-store/") && !/^https?/.test(assetPath)) {
assetFullPath = `/app-store/${appDirName}/${assetPath}`;
}
return assetFullPath;
}

View File

@ -1,7 +1,7 @@
---
items:
- /api/app-store/giphy/GIPHY1.png
- /api/app-store/giphy/GIPHY2.png
- GIPHY1.png
- GIPHY2.png
---
An online database and search engine that allows users to search for and share short looping videos with no sound that resemble animated GIF files. GIPHY is your top source for the best & newest GIFs & Animated Stickers online. Find everything from funny GIFs, reaction GIFs, unique GIFs and more to add to your custom booking page. Located under advanced settings in each event type.

View File

@ -7,9 +7,7 @@ export const metadata = {
description: _package.description,
installed: true,
categories: ["other"],
// If using static next public folder, can then be referenced from the base URL (/).
imageSrc: "/api/app-store/giphy/icon.svg",
logo: "/api/app-store/giphy/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
slug: "giphy",
title: "Giphy",

View File

@ -1,7 +1,7 @@
---
items:
- /api/app-store/googlecalendar/GCal1.png
- /api/app-store/googlecalendar/GCal2.png
- GCal1.png
- GCal2.png
---
Google Calendar is a time management and scheduling service developed by Google. Allows users to create and edit events, with options available for type and time. Available to anyone that has a Gmail account on both mobile and web versions.

View File

@ -12,7 +12,7 @@ export const metadata = {
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/googlecalendar/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
slug: "google-calendar",
url: "https://cal.com/",

View File

@ -1,7 +1,7 @@
---
items:
- /api/app-store/googlevideo/gmeet1.png
- /api/app-store/googlevideo/gmeet2.png
- gmeet1.png
- gmeet2.png
---
Google Meet is Google's web-based video conferencing platform, designed to compete with major conferencing platforms.

View File

@ -12,9 +12,8 @@ export const metadata = {
categories: ["video"],
type: "google_video",
title: "Google Meet",
imageSrc: "/api/app-store/googlevideo/logo.webp",
variant: "conferencing",
logo: "/api/app-store/googlevideo/logo.webp",
logo: "logo.webp",
publisher: "Cal.com",
url: "https://cal.com/",
isGlobal: false,

View File

@ -2,7 +2,6 @@
"name": "Google Tag Manager",
"slug": "gtm",
"type": "gtm_analytics",
"imageSrc": "icon.svg",
"logo": "icon.svg",
"url": "https://tagmanager.google.com",
"variant": "analytics",

View File

@ -1,6 +1,6 @@
---
items:
- /api/app-store/hubspot/hubspot01.webp
- hubspot01.webp
---
HubSpot is a cloud-based CRM designed to help align sales and marketing teams, foster sales enablement, boost ROI and optimize your inbound marketing strategy to generate more, qualified leads.

View File

@ -7,9 +7,8 @@ export const metadata = {
installed: !!process.env.HUBSPOT_CLIENT_ID,
description: _package.description,
type: "hubspot_other_calendar",
imageSrc: "/api/app-store/hubspot/icon.svg",
variant: "other_calendar",
logo: "/api/app-store/hubspot/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
url: "https://hubspot.com/",
categories: ["other"],

View File

@ -1,11 +1,11 @@
---
items:
- /api/app-store/huddle01video/1.png
- /api/app-store/huddle01video/2.png
- /api/app-store/huddle01video/3.png
- /api/app-store/huddle01video/4.png
- /api/app-store/huddle01video/5.png
- /api/app-store/huddle01video/6.png
- 1.png
- 2.png
- 3.png
- 4.png
- 5.png
- 6.png
---
Huddle01 is a new video conferencing software native to Web3 and is comparable to a decentralized version of Zoom. It supports conversations for NFT communities, DAOs, Builders and also has features such as token gating, NFTs as avatars, Web3 Login + ENS and recording over IPFS.

View File

@ -8,10 +8,9 @@ export const metadata = {
description: _package.description,
installed: true,
type: "huddle01_video",
imageSrc: "/api/app-store/huddle01video/icon.svg",
variant: "conferencing",
categories: ["video", "web3"],
logo: "/api/app-store/huddle01video/icon.svg",
logo: "icon.svg",
publisher: "huddle01.com",
url: "https://huddle01.com",
category: "web3",

View File

@ -1,6 +1,6 @@
---
items:
- /api/app-store/jitsivideo/jitsi1.jpg
- jitsi1.jpg
---
Jitsi is a free open-source video conferencing software for web and mobile. Make a call, launch on your own servers, integrate into your app, and more.

View File

@ -7,10 +7,9 @@ export const metadata = {
description: _package.description,
installed: true,
type: "jitsi_video",
imageSrc: "/api/app-store/jitsivideo/icon.svg",
variant: "conferencing",
categories: ["video"],
logo: "/api/app-store/jitsivideo/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
url: "https://jitsi.org/",
slug: "jitsi",

View File

@ -1,9 +1,9 @@
---
items:
- /api/app-store/larkcalendar/1.png
- /api/app-store/larkcalendar/2.png
- /api/app-store/larkcalendar/3.png
- /api/app-store/larkcalendar/4.png
- 1.png
- 2.png
- 3.png
- 4.png
---
<iframe class="w-full aspect-video" width="560" height="315" src="https://www.youtube.com/embed/ciqbZ466XSQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

View File

@ -8,10 +8,9 @@ export const metadata = {
installed: true,
type: "lark_calendar",
title: "Lark Calendar",
imageSrc: "/api/app-store/larkcalendar/icon.svg",
variant: "calendar",
categories: ["calendar"],
logo: "/api/app-store/larkcalendar/icon.svg",
logo: "icon.svg",
publisher: "Lark",
slug: "lark-calendar",
url: "https://larksuite.com/",

View File

@ -1,8 +1,8 @@
---
items:
- /api/app-store/n8n/1.png
- /api/app-store/n8n/2.png
- /api/app-store/n8n/3.png
- 1.png
- 2.png
- 3.png
- https://docs.n8n.io/_images/integrations/builtin/credentials/cal/getting-api-key.gif
---

View File

@ -3,8 +3,7 @@
"name": "n8n",
"slug": "n8n",
"type": "n8n_automation",
"imageSrc": "/api/app-store/n8n/icon.svg",
"logo": "/api/app-store/n8n/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/n8n",
"variant": "automation",
"categories": ["automation"],

View File

@ -1,9 +1,9 @@
---
items:
- /api/app-store/office365calendar/1.jpg
- /api/app-store/office365calendar/2.jpg
- /api/app-store/office365calendar/3.jpg
- /api/app-store/office365calendar/4.jpg
- 1.jpg
- 2.jpg
- 3.jpg
- 4.jpg
---
Microsoft Office 365 is a suite of apps that helps you stay connected with others and get things done. It includes but is not limited to Microsoft Word, PowerPoint, Excel, Teams, OneNote and OneDrive. Office 365 allows you to work remotely with others on a team and collaborate in an online environment. Both web versions and desktop/mobile applications are available.

View File

@ -7,13 +7,13 @@ export const metadata = {
description: _package.description,
type: "office365_calendar",
title: "Outlook Calendar",
imageSrc: "/api/app-store/office365calendar/icon.svg",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "/api/app-store/office365calendar/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
slug: "office365-calendar",
dirName: "office365calendar",
url: "https://cal.com/",
email: "help@cal.com",
} as AppMeta;

View File

@ -1,10 +1,10 @@
---
items:
- /api/app-store/office365video/teams1.png
- /api/app-store/office365video/teams2.png
- /api/app-store/office365video/teams3.jpeg
- /api/app-store/office365video/teams4.png
- /api/app-store/office365video/teams5.png
- teams1.png
- teams2.png
- teams3.jpeg
- teams4.png
- teams5.png
---
Microsoft Teams is a business communication platform and collaborative workspace included in Microsoft 365. It offers workspace chat and video conferencing, file storage, and application integration. Both web versions and desktop/mobile applications are available. NOTE: MUST HAVE A WORK / SCHOOL ACCOUNT

View File

@ -2,9 +2,8 @@
"name": "Microsoft 365/Teams (Requires work/school account)",
"description": "Microsoft Teams is a business communication platform and collaborative workspace included in Microsoft 365. It offers workspace chat and video conferencing, file storage, and application integration. Both web versions and desktop/mobile applications are available. NOTE: MUST HAVE A WORK / SCHOOL ACCOUNT",
"type": "office365_video",
"imageSrc": "/api/app-store/office365video/icon.svg",
"variant": "conferencing",
"logo": "/api/app-store/office365video/icon.svg",
"logo": "icon.svg",
"publisher": "Cal.com",
"url": "https://www.microsoft.com/en-ca/microsoft-teams/group-chat-software",
"verified": true,

View File

@ -1,8 +1,8 @@
---
items:
- /api/app-store/ping/1.png
- /api/app-store/ping/2.png
- /api/app-store/ping/3.png
- 1.png
- 2.png
- 3.png
---
Ping.gg makes high quality video collaborations easier than ever. Think "Zoom for streamers and creators". Join a call in 3 clicks, manage audio and video like a pro, and copy-paste your guests straight into OBS

View File

@ -4,8 +4,7 @@
"title": "Ping.gg",
"slug": "ping",
"type": "ping_video",
"imageSrc": "/api/app-store/ping/icon.svg",
"logo": "/api/app-store/ping/icon.svg",
"logo": "icon.svg",
"url": "https://ping.gg",
"variant": "conferencing",
"categories": ["video"],

View File

@ -1,11 +1,11 @@
---
description: Connect APIs, remarkably fast. Stop writing boilerplate code, struggling with authentication and managing infrastructure. Start connecting APIs with code-level control when you need it — and no code when you don't
items:
- /api/app-store/pipedream/1.png
- /api/app-store/pipedream/2.png
- /api/app-store/pipedream/3.png
- /api/app-store/pipedream/4.png
- /api/app-store/pipedream/5.png
- 1.png
- 2.png
- 3.png
- 4.png
- 5.png
---
Connect APIs, remarkably fast. Stop writing boilerplate code, struggling with authentication and managing infrastructure. Start connecting APIs with code-level control when you need it — and no code when you don't

View File

@ -3,8 +3,7 @@
"name": "Pipedream",
"slug": "pipedream",
"type": "pipedream_automation",
"imageSrc": "/api/app-store/pipedream/icon.svg",
"logo": "/api/app-store/pipedream/icon.svg",
"logo": "icon.svg",
"url": "https://pipedream.com/apps/cal-com",
"variant": "automation",
"categories": ["automation"],

View File

@ -1,6 +1,6 @@
---
items:
- /api/app-store/plausible/1.jpg
- 1.jpg
---
Simple, privacy-friendly Google Analytics alternative.

View File

@ -3,8 +3,7 @@
"name": "Plausible",
"slug": "plausible",
"type": "plausible_analytics",
"imageSrc": "/api/app-store/plausible/icon.svg",
"logo": "/api/app-store/plausible/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/plausible",
"variant": "analytics",
"categories": ["analytics"],

View File

@ -3,8 +3,7 @@
"name": "QR Code",
"slug": "qr_code",
"type": "qr_code_other",
"imageSrc": "/api/app-store/qr_code/icon.svg",
"logo": "/api/app-store/qr_code/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/qr_code",
"variant": "other",
"categories": ["other"],

View File

@ -1,8 +1,8 @@
---
items:
- /api/app-store/rainbow/1.jpg
- /api/app-store/rainbow/2.jpg
- /api/app-store/rainbow/3.jpg
- 1.jpg
- 2.jpg
- 3.jpg
---
Token gate bookings based on NFTs, DAO tokens, and ERC-20 tokens. Rainbow supports dozens of trusted Ethereum wallet apps to verify token ownership. Available blockchains are Ethereum mainnet, Arbitrum, Optimism, and Polygon mainnet.

View File

@ -3,8 +3,7 @@
"name": "Rainbow",
"slug": "rainbow",
"type": "rainbow_web3",
"imageSrc": "/api/app-store/rainbow/icon.svg",
"logo": "/api/app-store/rainbow/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/rainbow",
"variant": "web3",
"categories": ["web3"],

View File

@ -0,0 +1,25 @@
import { checkBalance } from "../utils/ethereum";
import type { TBalanceInputSchema } from "./balance.schema";
interface BalanceHandlerOptions {
input: TBalanceInputSchema;
}
export const balanceHandler = async ({ input }: BalanceHandlerOptions) => {
const { address, tokenAddress, chainId } = input;
try {
const hasBalance = await checkBalance(address, tokenAddress, chainId);
return {
data: {
hasBalance,
},
};
} catch (e) {
return {
data: {
hasBalance: false,
},
};
}
};

View File

@ -0,0 +1,19 @@
import z from "zod";
export const ZBalanceInputSchema = z.object({
address: z.string(),
tokenAddress: z.string(),
chainId: z.number(),
});
export const ZBalanceOutputSchema = z.object({
data: z
.object({
hasBalance: z.boolean(),
})
.nullish(),
error: z.string().nullish(),
});
export type TBalanceOutputSchema = z.infer<typeof ZBalanceOutputSchema>;
export type TBalanceInputSchema = z.infer<typeof ZBalanceInputSchema>;

View File

@ -0,0 +1,42 @@
import { ethers } from "ethers";
import { configureChains, createClient } from "wagmi";
import abi from "../utils/abi.json";
import { getProviders, SUPPORTED_CHAINS } from "../utils/ethereum";
import type { TContractInputSchema } from "./contract.schema";
interface ContractHandlerOptions {
input: TContractInputSchema;
}
export const contractHandler = async ({ input }: ContractHandlerOptions) => {
const { address, chainId } = input;
const { provider } = configureChains(
SUPPORTED_CHAINS.filter((chain) => chain.id === chainId),
getProviders()
);
const client = createClient({
provider,
});
const contract = new ethers.Contract(address, abi, client.provider);
try {
const name = await contract.name();
const symbol = await contract.symbol();
return {
data: {
name,
symbol: `$${symbol}`,
},
};
} catch (e) {
return {
data: {
name: address,
symbol: "$UNKNOWN",
},
};
}
};

View File

@ -0,0 +1,19 @@
import z from "zod";
export const ZContractInputSchema = z.object({
address: z.string(),
chainId: z.number(),
});
export const ZContractOutputSchema = z.object({
data: z
.object({
name: z.string(),
symbol: z.string(),
})
.nullish(),
error: z.string().nullish(),
});
export type TContractInputSchema = z.infer<typeof ZContractInputSchema>;
export type TContractOutputSchema = z.infer<typeof ZContractOutputSchema>;

View File

@ -1,100 +1,53 @@
import { ethers } from "ethers";
import { configureChains, createClient } from "wagmi";
import { z } from "zod";
import { router, publicProcedure } from "@calcom/trpc/server/trpc";
import abi from "../utils/abi.json";
import { checkBalance, getProviders, SUPPORTED_CHAINS } from "../utils/ethereum";
import { ZBalanceInputSchema, ZBalanceOutputSchema } from "./balance.schema";
import { ZContractInputSchema, ZContractOutputSchema } from "./contract.schema";
interface EthRouterHandlersCache {
contract?: typeof import("./contract.handler").contractHandler;
balance?: typeof import("./balance.handler").balanceHandler;
}
const UNSTABLE_HANDLER_CACHE: EthRouterHandlersCache = {};
const ethRouter = router({
// Fetch contract `name` and `symbol` or error
contract: publicProcedure
.input(
z.object({
address: z.string(),
chainId: z.number(),
})
)
.output(
z.object({
data: z
.object({
name: z.string(),
symbol: z.string(),
})
.nullish(),
error: z.string().nullish(),
})
)
.input(ZContractInputSchema)
.output(ZContractOutputSchema)
.query(async ({ input }) => {
const { address, chainId } = input;
const { provider } = configureChains(
SUPPORTED_CHAINS.filter((chain) => chain.id === chainId),
getProviders()
);
const client = createClient({
provider,
});
const contract = new ethers.Contract(address, abi, client.provider);
try {
const name = await contract.name();
const symbol = await contract.symbol();
return {
data: {
name,
symbol: `$${symbol}`,
},
};
} catch (e) {
return {
data: {
name: address,
symbol: "$UNKNOWN",
},
};
if (!UNSTABLE_HANDLER_CACHE.contract) {
UNSTABLE_HANDLER_CACHE.contract = await import("./contract.handler").then(
(mod) => mod.contractHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.contract) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.contract({
input,
});
}),
// Fetch user's `balance` of either ERC-20 or ERC-721 compliant token or error
balance: publicProcedure
.input(
z.object({
address: z.string(),
tokenAddress: z.string(),
chainId: z.number(),
})
)
.output(
z.object({
data: z
.object({
hasBalance: z.boolean(),
})
.nullish(),
error: z.string().nullish(),
})
)
.input(ZBalanceInputSchema)
.output(ZBalanceOutputSchema)
.query(async ({ input }) => {
const { address, tokenAddress, chainId } = input;
try {
const hasBalance = await checkBalance(address, tokenAddress, chainId);
return {
data: {
hasBalance,
},
};
} catch (e) {
return {
data: {
hasBalance: false,
},
};
if (!UNSTABLE_HANDLER_CACHE.balance) {
UNSTABLE_HANDLER_CACHE.balance = await import("./balance.handler").then((mod) => mod.balanceHandler);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.balance) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.balance({
input,
});
}),
});

View File

@ -1,9 +1,9 @@
---
items:
- /api/app-store/raycast/1.png
- /api/app-store/raycast/2.png
- /api/app-store/raycast/3.png
- /api/app-store/raycast/4.png
- 1.png
- 2.png
- 3.png
- 4.png
---
Quickly share your Cal.com meeting links with Raycast. Requires Raycast.com to be installed. You can create an API token in your Developer Cal.com Settings.

View File

@ -3,8 +3,7 @@
"name": "Raycast",
"slug": "raycast",
"type": "raycast_other",
"imageSrc": "/api/app-store/raycast/icon.svg",
"logo": "/api/app-store/raycast/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/raycast",
"variant": "other",
"categories": ["other"],

View File

@ -1,6 +1,6 @@
---
items:
- /api/app-store/riverside/riverside1.png
- riverside1.png
---
Your online recording studio. The easiest way to record podcasts and videos in studio quality from anywhere. All from the browser.

View File

@ -3,8 +3,7 @@
"name": "Riverside",
"slug": "riverside",
"type": "riverside_video",
"imageSrc": "/api/app-store/riverside/icon-dark.svg",
"logo": "/api/app-store/riverside/icon-dark.svg",
"logo": "icon-dark.svg",
"url": "https://cal.com/apps/riverside",
"variant": "conferencing",
"categories": ["video"],

View File

@ -1,8 +1,8 @@
---
items:
- /api/app-store/routing-forms/1.jpg
- /api/app-store/routing-forms/2.jpg
- /api/app-store/routing-forms/3.jpg
- 1.jpg
- 2.jpg
- 3.jpg
---
It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user

View File

@ -4,8 +4,7 @@
"title": "Routing Forms",
"slug": "routing-forms",
"type": "routing-forms_other",
"imageSrc": "/api/app-store/routing-forms/icon-dark.svg",
"logo": "/api/app-store/routing-forms/icon-dark.svg",
"logo": "icon-dark.svg",
"url": "https://cal.com/apps/routing-forms",
"variant": "other",
"categories": ["other"],

View File

@ -1,7 +1,7 @@
---
description: Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.
items:
- /api/app-store/salesforce/1.png
- 1.png
---
Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.

View File

@ -3,8 +3,7 @@
"name": "Salesforce",
"slug": "salesforce",
"type": "salesforce_other_calendar",
"imageSrc": "/api/app-store/salesforce/icon.png",
"logo": "/api/app-store/salesforce/icon.png",
"logo": "icon.png",
"url": "https://cal.com/apps/salesforce",
"variant": "other_calendar",
"categories": ["other"],

View File

@ -1,7 +1,7 @@
---
description: SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.
items:
- /api/app-store/sendgrid/1.png
- 1.png
---
SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.

View File

@ -3,8 +3,7 @@
"name": "Sendgrid",
"slug": "sendgrid",
"type": "sendgrid_other_calendar",
"imageSrc": "/api/app-store/sendgrid/logo.png",
"logo": "/api/app-store/sendgrid/logo.png",
"logo": "logo.png",
"url": "https://cal.com/apps/sendgrid",
"variant": "other_calendar",
"categories": ["other"],

View File

@ -1,8 +1,8 @@
---
description: Schedule a chat with your guests or have a Signal Video call.
items:
- /api/app-store/signal/1.jpg
- /api/app-store/signal/2.jpg
- 1.jpg
- 2.jpg
---
Schedule a chat with your guests or have a Signal Video call.

View File

@ -3,8 +3,7 @@
"name": "Signal",
"slug": "signal",
"type": "signal_video",
"imageSrc": "/api/app-store/signal/icon.svg",
"logo": "/api/app-store/signal/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/signal",
"variant": "conferencing",
"categories": ["video"],

View File

@ -1,9 +1,9 @@
---
description: Video meetings made for music. Create your own virtual music classroom, easily.
items:
- /api/app-store/sirius_video/1.jpg
- /api/app-store/sirius_video/2.jpg
- /api/app-store/sirius_video/3.jpg
- 1.jpg
- 2.jpg
- 3.jpg
---
Video meetings made for music. Create your own virtual music classroom, easily.

View File

@ -3,8 +3,7 @@
"name": "Sirius Video",
"slug": "sirius_video",
"type": "sirius_video_video",
"imageSrc": "/api/app-store/sirius_video/icon-dark.svg",
"logo": "/api/app-store/sirius_video/icon-dark.svg",
"logo": "icon-dark.svg",
"url": "https://cal.com/apps/sirius_video",
"variant": "conferencing",
"categories": ["video"],

View File

@ -1,10 +1,10 @@
---
items:
- /api/app-store/stripepayment/stripe1.jpg
- /api/app-store/stripepayment/stripe2.jpg
- /api/app-store/stripepayment/stripe3.jpg
- /api/app-store/stripepayment/stripe4.jpg
- /api/app-store/stripepayment/stripe5.jpg
- stripe1.jpg
- stripe2.jpg
- stripe3.jpg
- stripe4.jpg
- stripe5.jpg
---
Stripe provides payment infrastructure for everyone from startups to Fortune 500 companies. They provide payment processing software as well as application programming interfaces (APIs) for mobile applications as well as e-commerce websites processing payments from (but not limited to) credit cards, debit cards, digital wallets, Google Pay, Apple Pay, Bank Transfers, Alipay and WeChat.

View File

@ -13,8 +13,7 @@ export const metadata = {
slug: "stripe",
category: "payment",
categories: ["payment"],
logo: "/api/app-store/stripepayment/icon.svg",
imageSrc: "/api/app-store/stripepayment/icon.svg",
logo: "icon.svg",
publisher: "Cal.com",
title: "Stripe",
type: "stripe_payment",

View File

@ -113,7 +113,7 @@ export class PaymentService implements IAbstractPaymentService {
}
return paymentData;
} catch (error) {
console.error(error);
console.error(`Payment could not be created for bookingId ${bookingId}`, error);
throw new Error("Payment could not be created");
}
}
@ -124,75 +124,80 @@ export class PaymentService implements IAbstractPaymentService {
bookerEmail: string,
paymentOption: PaymentOption
): Promise<Payment> {
// Ensure that the payment service can support the passed payment option
if (paymentOptionEnum.parse(paymentOption) !== "HOLD") {
throw new Error("Payment option is not compatible with create method");
try {
// Ensure that the payment service can support the passed payment option
if (paymentOptionEnum.parse(paymentOption) !== "HOLD") {
throw new Error("Payment option is not compatible with create method");
}
// Load stripe keys
const stripeAppKeys = await prisma?.app.findFirst({
select: {
keys: true,
},
where: {
slug: "stripe",
},
});
// Parse keys with zod
const { payment_fee_fixed, payment_fee_percentage } = stripeAppKeysSchema.parse(stripeAppKeys?.keys);
const paymentFee = Math.round(payment.amount * payment_fee_percentage + payment_fee_fixed);
const customer = await retrieveOrCreateStripeCustomerByEmail(
bookerEmail,
this.credentials.stripe_user_id
);
const params = {
customer: customer.id,
payment_method_types: ["card"],
metadata: {
bookingId,
},
};
const setupIntent = await this.stripe.setupIntents.create(params, {
stripeAccount: this.credentials.stripe_user_id,
});
const paymentData = await prisma?.payment.create({
data: {
uid: uuidv4(),
app: {
connect: {
slug: "stripe",
},
},
booking: {
connect: {
id: bookingId,
},
},
amount: payment.amount,
currency: payment.currency,
externalId: setupIntent.id,
data: Object.assign(
{},
{
setupIntent,
stripe_publishable_key: this.credentials.stripe_publishable_key,
stripeAccount: this.credentials.stripe_user_id,
}
) as unknown as Prisma.InputJsonValue,
fee: paymentFee,
refunded: false,
success: false,
paymentOption: paymentOption || "ON_BOOKING",
},
});
return paymentData;
} catch (error) {
console.error(`Payment method could not be collected for bookingId ${bookingId}`, error);
throw new Error("Payment could not be created");
}
// Load stripe keys
const stripeAppKeys = await prisma?.app.findFirst({
select: {
keys: true,
},
where: {
slug: "stripe",
},
});
// Parse keys with zod
const { payment_fee_fixed, payment_fee_percentage } = stripeAppKeysSchema.parse(stripeAppKeys?.keys);
const paymentFee = Math.round(payment.amount * payment_fee_percentage + payment_fee_fixed);
const customer = await retrieveOrCreateStripeCustomerByEmail(
bookerEmail,
this.credentials.stripe_user_id
);
const params = {
customer: customer.id,
payment_method_types: ["card"],
metadata: {
bookingId,
},
};
const setupIntent = await this.stripe.setupIntents.create(params, {
stripeAccount: this.credentials.stripe_user_id,
});
const paymentData = await prisma?.payment.create({
data: {
uid: uuidv4(),
app: {
connect: {
slug: "stripe",
},
},
booking: {
connect: {
id: bookingId,
},
},
amount: payment.amount,
currency: payment.currency,
externalId: setupIntent.id,
data: Object.assign(
{},
{
setupIntent,
stripe_publishable_key: this.credentials.stripe_publishable_key,
stripeAccount: this.credentials.stripe_user_id,
}
) as unknown as Prisma.InputJsonValue,
fee: paymentFee,
refunded: false,
success: false,
paymentOption: paymentOption || "ON_BOOKING",
},
});
return paymentData;
}
async chargeCard(payment: Payment): Promise<Payment> {
@ -264,7 +269,7 @@ export class PaymentService implements IAbstractPaymentService {
return paymentData;
} catch (error) {
console.error(error);
console.error(`Could not charge card for payment ${payment.id}`, error);
throw new Error("Payment could not be created");
}
}

View File

@ -4,7 +4,6 @@
"title": "Sylaps",
"slug": "sylapsvideo",
"type": "sylaps_video",
"imageSrc": "icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/sylaps",
"variant": "conferencing",

View File

@ -1,11 +1,11 @@
---
items:
- /api/app-store/tandemvideo/tandem1.jpg
- /api/app-store/tandemvideo/tandem2.jpg
- /api/app-store/tandemvideo/tandem3.jpg
- /api/app-store/tandemvideo/tandem4.jpg
- /api/app-store/tandemvideo/tandem5.jpg
- /api/app-store/tandemvideo/tandem6.jpg
- tandem1.jpg
- tandem2.jpg
- tandem3.jpg
- tandem4.jpg
- tandem5.jpg
- tandem6.jpg
---
Tandem is a new virtual office space that allows teams to effortlessly connect as though they are in a physical office, online. Through co-working rooms, available statuses, live real-time video call, and chat options, you can see whos around, talk and collaborate in one click. It works cross-platform with both desktop and mobile versions.

View File

@ -7,12 +7,11 @@ export const metadata = {
description: _package.description,
type: "tandem_video",
title: "Tandem Video",
imageSrc: "/api/app-store/tandemvideo/icon.svg",
variant: "conferencing",
categories: ["video"],
slug: "tandem",
category: "video",
logo: "/api/app-store/tandemvideo/icon.svg",
logo: "icon.svg",
publisher: "",
url: "",
isGlobal: false,

View File

@ -1,8 +1,8 @@
---
items:
- /api/app-store/telegram/1.jpg
- /api/app-store/telegram/2.jpg
- /api/app-store/telegram/3.jpg
- 1.jpg
- 2.jpg
- 3.jpg
---
Schedule a chat with your guests or have a Telegram Video call.

View File

@ -3,8 +3,7 @@
"name": "Telegram",
"slug": "telegram",
"type": "telegram_video",
"imageSrc": "/api/app-store/telegram/icon.svg",
"logo": "/api/app-store/telegram/icon.svg",
"logo": "icon.svg",
"url": "https://cal.com/apps/telegram",
"variant": "conferencing",
"categories": ["video"],

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