feat: validate `CalProvider` api keys (#12672)

* fix: signup nit (#12585)

* Disable submit on empty form

* Fix submit

---------

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

* New Crowdin translations by Github Action

* fix: Signup options are not disabled (#12610)

* fix: Signup options are not disabled

* fixes/signup disabling suggested changes done

* chore: signup and login improvements

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>

* fix: typo in @calcom/emails readme (#12615)

* fix: handle reschedule request for dynamic meetings (#12275)

* chore: [app-router-migration-1] migrate the pages in `settings/admin` to the app directory (#12561)

Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>

* chore: added cursor-pointer to img upload (#12624)

* added cursor-pointer to img upload

* nit

* feat: add clear filters option in bookings page (#12629)

* add clear filters option

* fix vscode settings.json

* use removeAllQueryParams()

* fix yarn lock

* remove toggleoption

* feat: display long durations in hours on booking (#12631)

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* New Crowdin translations by Github Action

* fix: workaround for future app dir routes

* feat: Allow only first slot to be booked (#12636)

Co-authored-by: Morgan Vernay <morgan@cal.com>

* New Crowdin translations by Github Action

* feat: add matomo analytics app (#12646)

* chore: Sentry Wrapper with Performance and Error Tracing (#12642)

* add wrapper for sentry and update functions in 'getUserAvailability'. Update tracesSampleRate to 1.0

* Make Sentry Wrapper utilize parent transaction, if it exists.

* Update wrapper for functions to inherit parameters from the child function

* add comment of when to use the wrapper

* check for sentry before wrapping, if not call unwrapped function

* refactored wrapper to have async and sync separate functions that utilize helpers for common behaviour

* update type of args to unknown

* fixed types of returns from wrapped functions

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>

* validate api keys to set error and key states for api keys

* organize error messages into one place

* set error messages from errors file instead of hardcoding value

* fix incorrect constant name

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Pratik Kumar <70286186+Pratik-Kumar-621@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Samyabrata Maji <116789799+samyabrata-maji@users.noreply.github.com>
Co-authored-by: Manpreet Singh <manpoffc@gmail.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Varun Prahlad Balani <varunprahladbalani@gmail.com>
Co-authored-by: Mike Zhou <mikezhoudev@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Haran Rajkumar <haranrajkumar97@gmail.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Harshith Pabbati <pabbatiharshith@gmail.com>
Co-authored-by: Brendan Woodward <73412688+bwoody13@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Rajiv Sahal 2023-12-04 14:59:25 +05:30 committed by GitHub
parent f0401e1f86
commit ffba6d6d66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
153 changed files with 1714 additions and 220 deletions

View File

@ -290,3 +290,4 @@ E2E_TEST_OIDC_USER_PASSWORD=
AB_TEST_BUCKET_PROBABILITY=50
# whether we redirect to the future/event-types from event-types or not
APP_ROUTER_EVENT_TYPES_ENABLED=1
APP_ROUTER_SETTINGS_ADMIN_ENABLED=1

View File

@ -24,6 +24,8 @@ runs:
**/.turbo/**
**/dist/**
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
- run: yarn build
- run: |
export NODE_OPTIONS="--max_old_space_size=8192"
yarn build
if: steps.cache-build.outputs.cache-hit != 'true'
shell: bash

View File

@ -60,6 +60,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
successRedirectUrl: true,
locations: true,
bookingLimits: true,
onlyShowFirstAvailableSlot: true,
durationLimits: true,
})
.merge(
@ -147,6 +148,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
seatsShowAvailabilityCount: true,
bookingFields: true,
bookingLimits: true,
onlyShowFirstAvailableSlot: true,
durationLimits: true,
}).merge(
z.object({

View File

@ -5,6 +5,7 @@ import z from "zod";
const ROUTES: [URLPattern, boolean][] = [
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
].map(([pathname, enabled]) => [
new URLPattern({
pathname,
@ -27,7 +28,6 @@ export const abTestMiddlewareFactory =
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
const enabled = route !== null ? route[1] || override : false;
if (pathname.includes("future") || !enabled) {

View File

@ -0,0 +1,4 @@
import type { TRPCContext } from "@calcom/trpc/server/createContext";
import { appRouter } from "@calcom/trpc/server/routers/_app";
export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx);

View File

@ -8,33 +8,8 @@ import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
import { httpLink } from "@calcom/trpc/client/links/httpLink";
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
import { splitLink } from "@calcom/trpc/client/links/splitLink";
import { ENDPOINTS } from "@calcom/trpc/react/shared";
const ENDPOINTS = [
"admin",
"apiKeys",
"appRoutingForms",
"apps",
"auth",
"availability",
"appBasecamp3",
"bookings",
"deploymentSetup",
"eventTypes",
"features",
"insights",
"payments",
"public",
"saml",
"slots",
"teams",
"organizations",
"users",
"viewer",
"webhook",
"workflows",
"appsRouter",
"googleWorkspace",
] as const;
export type Endpoint = (typeof ENDPOINTS)[number];
// eslint-disable-next-line @typescript-eslint/no-explicit-any

3
apps/web/app/_types.ts Normal file
View File

@ -0,0 +1,3 @@
export type Params = {
[param: string]: string | string[] | undefined;
};

View File

@ -0,0 +1,20 @@
import { headers } from "next/headers";
import { type ReactElement } from "react";
import PageWrapper from "@components/PageWrapperAppDir";
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
type WrapperWithLayoutProps = {
children: ReactElement;
};
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/admin/apps/[category]";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("apps"),
(t) => t("admin_apps_description")
);
export default Page;

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/admin/apps/index";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("apps"),
(t) => t("admin_apps_description")
);
export default Page;

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/admin/flags";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Feature Flags",
() => "Here you can toggle your Cal.com instance features."
);
export default Page;

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/admin/impersonation";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("admin"),
(t) => t("impersonation")
);
export default Page;

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/admin/oAuth/index";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "OAuth",
() => "Add new OAuth Clients"
);
export default Page;

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/admin/index";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Admin",
() => "admin_description"
);
export default Page;

View File

@ -0,0 +1,20 @@
// pages containing layout (e.g., /availability/[schedule].tsx) are supposed to go under (no-layout) folder
import { headers } from "next/headers";
import { type ReactElement } from "react";
import PageWrapper from "@components/PageWrapperAppDir";
type WrapperWithoutLayoutProps = {
children: ReactElement;
};
export default async function WrapperWithoutLayout({ children }: WrapperWithoutLayoutProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/admin/oAuth/oAuthView";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "OAuth",
() => "Add new OAuth Clients"
);
export default Page;

View File

@ -0,0 +1,21 @@
import { headers } from "next/headers";
import { type ReactElement } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
import PageWrapper from "@components/PageWrapperAppDir";
type WrapperWithLayoutProps = {
children: ReactElement;
};
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
{children}
</PageWrapper>
);
}

View File

@ -0,0 +1,13 @@
import Page from "@pages/settings/admin/organizations/index";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("organizations"),
(t) => t("orgs_page_description")
);
export default function AppPage() {
// @ts-expect-error FIXME Property 'Component' is incompatible with index signature
return <Page />;
}

View File

@ -0,0 +1,39 @@
import Page from "@pages/settings/admin/users/[id]/edit";
import { getServerCaller } from "app/_trpc/serverClient";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { z } from "zod";
import prisma from "@calcom/prisma";
const userIdSchema = z.object({ id: z.coerce.number() });
export const generateMetadata = async ({ params }: { params: Params }) => {
const input = userIdSchema.safeParse(params);
let title = "";
if (!input.success) {
title = "Editing user";
} else {
const req = {
headers: headers(),
cookies: cookies(),
};
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest'
const data = await getServerCaller({ req, prisma }).viewer.users.get({ userId: input.data.id });
const { user } = data;
title = `Editing user: ${user.username}`;
}
return await _generateMetadata(
() => title,
() => "Here you can edit a current user."
);
};
export default function AppPage() {
// @ts-expect-error FIXME AppProps | undefined' does not satisfy the constraint 'PageProps'
return <Page />;
}

View File

@ -0,0 +1,13 @@
import Page from "@pages/settings/admin/users/add";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Add new user",
() => "Here you can add a new user."
);
export default function AppPage() {
// @ts-expect-error FIXME AppProps | undefined' does not satisfy the constraint 'PageProps'
return <Page />;
}

View File

@ -0,0 +1,13 @@
import Page from "@pages/settings/admin/users/index";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Users",
() => "A list of all the users in your account including their name, title, email and role."
);
export default function AppPage() {
// @ts-expect-error FIXME Property 'Component' is incompatible with index signature
return <Page />;
}

View File

@ -0,0 +1,40 @@
"use client";
import { useSession } from "next-auth/react";
import { usePathname, useRouter } from "next/navigation";
import type { ComponentProps } from "react";
import React, { useEffect } from "react";
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
import type Shell from "@calcom/features/shell/Shell";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { ErrorBoundary } from "@calcom/ui";
export default function AdminLayout({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
const pathname = usePathname();
const session = useSession();
const router = useRouter();
// Force redirect on component level
useEffect(() => {
if (session.data && session.data.user.role !== UserPermissionRole.ADMIN) {
router.replace("/settings/my-account/profile");
}
}, [session, router]);
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
return (
<SettingsLayout {...rest}>
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
<ErrorBoundary>{children}</ErrorBoundary>
</div>
</div>
</SettingsLayout>
);
}
export const getLayout = (page: React.ReactElement) => <AdminLayout>{page}</AdminLayout>;

View File

@ -298,6 +298,29 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
);
}}
/>
<Controller
name="onlyShowFirstAvailableSlot"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = value;
return (
<SettingsToggle
toggleSwitchAtTheEnd={true}
labelClassName="text-sm"
title={t("limit_booking_only_first_slot")}
description={t("limit_booking_only_first_slot_description")}
checked={isChecked}
onCheckedChange={(active) => {
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
}}
switchContainerClassName={classNames(
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
isChecked && "rounded-b-none"
)}
/>
);
}}
/>
<Controller
name="durationLimits"
control={formMethods.control}

View File

@ -101,6 +101,8 @@ export const config = {
"/apps/routing_forms/:path*",
"/event-types",
"/future/event-types/",
"/settings/admin/:path*",
"/future/settings/admin/:path*",
],
};

View File

@ -231,6 +231,9 @@ const nextConfig = {
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
// by next.js will be dropped. Doesn't make much sense, but how it is
fs: false,
// ignore module resolve errors caused by the server component bundler
"pg-native": false,
"superagent-proxy": false,
};
/**

View File

@ -13,7 +13,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("MonthlyDigestEmail", {
await renderEmail("MonthlyDigestEmail", {
language: t,
Created: 12,
Completed: 13,

View File

@ -251,6 +251,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
<Button
color="secondary"
className="w-full justify-center"
disabled={formState.isSubmitting}
data-testid="google"
StartIcon={FaGoogle}
onClick={async (e) => {

View File

@ -131,6 +131,7 @@ export type FormValues = {
successRedirectUrl: string;
durationLimits?: IntervalLimit;
bookingLimits?: IntervalLimit;
onlyShowFirstAvailableSlot: boolean;
children: ChildrenEventType[];
hosts: { userId: number; isFixed: boolean }[];
bookingFields: z.infer<typeof eventTypeBookingFields>;
@ -250,6 +251,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
description: eventType.description ?? undefined,
schedule: eventType.schedule || undefined,
bookingLimits: eventType.bookingLimits || undefined,
onlyShowFirstAvailableSlot: eventType.onlyShowFirstAvailableSlot || undefined,
durationLimits: eventType.durationLimits || undefined,
length: eventType.length,
hidden: eventType.hidden,
@ -429,6 +431,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits,
onlyShowFirstAvailableSlot,
durationLimits,
recurringEvent,
locations,
@ -491,6 +494,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,
bookingLimits,
onlyShowFirstAvailableSlot,
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
@ -532,6 +536,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsShowAttendees,
seatsShowAvailabilityCount,
bookingLimits,
onlyShowFirstAvailableSlot,
durationLimits,
recurringEvent,
locations,
@ -584,6 +589,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,
bookingLimits,
onlyShowFirstAvailableSlot,
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,

View File

@ -1,3 +1,5 @@
"use client";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta } from "@calcom/ui";

View File

@ -1 +1,2 @@
"use client";
export { default } from "./[category]";

View File

@ -1,3 +1,5 @@
"use client";
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
import PageWrapper from "@components/PageWrapper";

View File

@ -1,3 +1,5 @@
"use client";
import { signIn } from "next-auth/react";
import { useRef } from "react";

View File

@ -1,3 +1,5 @@
"use client";
import { Meta } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";

View File

@ -1,3 +1,5 @@
"use client";
import PageWrapper from "@components/PageWrapper";
import { getLayout } from "@components/auth/layouts/AdminLayout";

View File

@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";

View File

@ -1,3 +1,5 @@
"use client";
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
import type { CalPageWrapper } from "@components/PageWrapper";

View File

@ -1,3 +1,5 @@
"use client";
import UsersEditView from "@calcom/features/ee/users/pages/users-edit-view";
import type { CalPageWrapper } from "@components/PageWrapper";

View File

@ -1,3 +1,5 @@
"use client";
import UsersAddView from "@calcom/features/ee/users/pages/users-add-view";
import type { CalPageWrapper } from "@components/PageWrapper";

View File

@ -1,3 +1,5 @@
"use client";
import UsersListingView from "@calcom/features/ee/users/pages/users-listing-view";
import type { CalPageWrapper } from "@components/PageWrapper";

View File

@ -122,21 +122,23 @@ function UsernameField({
/>
{(!formState.isSubmitting || !formState.isSubmitted) && (
<div className="text-gray text-default flex items-center text-sm">
<p className="flex items-center text-sm ">
<div className="text-sm ">
{usernameTaken ? (
<div className="text-error">
<div className="text-error flex items-center">
<Info className="mr-1 inline-block h-4 w-4" />
{t("already_in_use_error")}
<p>{t("already_in_use_error")}</p>
</div>
) : premium ? (
<div data-testid="premium-username-warning">
<div data-testid="premium-username-warning" className="flex items-center">
<StarIcon className="mr-1 inline-block h-4 w-4" />
{t("premium_username", {
price: getPremiumPlanPriceValue(),
})}
<p>
{t("premium_username", {
price: getPremiumPlanPriceValue(),
})}
</p>
</div>
) : null}
</p>
</div>
</div>
)}
</div>
@ -161,6 +163,7 @@ export default function Signup({
}: SignupProps) {
const [premiumUsername, setPremiumUsername] = useState(false);
const [usernameTaken, setUsernameTaken] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const searchParams = useCompatSearchParams();
const telemetry = useTelemetry();
@ -245,21 +248,21 @@ export default function Signup({
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}>
<div className="bg-muted 2xl:border-subtle grid max-h-[800px] w-full max-w-[1440px] grid-cols-1 grid-rows-1 lg:grid-cols-2 2xl:rounded-lg 2xl:border ">
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
<div className="flex w-full flex-col px-4 py-6 sm:px-16 md:px-24 2xl:px-28">
<div className="flex w-full flex-col px-4 pt-6 sm:px-16 md:px-20 2xl:px-28">
{/* Header */}
{errors.apiError && (
<Alert severity="error" message={errors.apiError?.message} data-testid="signup-error-message" />
)}
<div className="flex flex-col gap-1">
<h1 className="font-cal text-[28px] ">
<div className="flex flex-col gap-2">
<h1 className="font-cal text-[28px] leading-none ">
{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
</h1>
{IS_CALCOM ? (
<p className="text-subtle text-base font-medium leading-6">{t("cal_signup_description")}</p>
<p className="text-subtle text-base font-medium leading-5">{t("cal_signup_description")}</p>
) : (
<p className="text-subtle text-base font-medium leading-6">
<p className="text-subtle text-base font-medium leading-5">
{t("calcom_explained", {
appName: APP_NAME,
})}
@ -318,6 +321,9 @@ export default function Signup({
disabled={
!!formMethods.formState.errors.username ||
!!formMethods.formState.errors.email ||
!formMethods.getValues("email") ||
!formMethods.getValues("password") ||
isSubmitting ||
usernameTaken
}>
{premiumUsername && !usernameTaken
@ -344,11 +350,22 @@ export default function Signup({
<Button
color="secondary"
disabled={!!formMethods.formState.errors.username || premiumUsername}
loading={isGoogleLoading}
StartIcon={() => (
<>
<img
className={classNames("text-subtle mr-2 h-4 w-4", premiumUsername && "opacity-50")}
src="/google-icon.svg"
alt=""
/>
</>
)}
className={classNames(
"w-full justify-center rounded-md text-center",
formMethods.formState.errors.username ? "opacity-50" : ""
)}
onClick={async () => {
setIsGoogleLoading(true);
const username = formMethods.getValues("username");
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL;
const GOOGLE_AUTH_URL = `${baseUrl}/auth/sso/google`;
@ -362,11 +379,6 @@ export default function Signup({
}
router.push(GOOGLE_AUTH_URL);
}}>
<img
className={classNames("text-emphasis mr-2 h-5 w-5", premiumUsername && "opacity-50")}
src="/google-icon.svg"
alt=""
/>
Google
</Button>
) : null}
@ -376,7 +388,9 @@ export default function Signup({
disabled={
!!formMethods.formState.errors.username ||
!!formMethods.formState.errors.email ||
premiumUsername
premiumUsername ||
isSubmitting ||
isGoogleLoading
}
className={classNames(
"w-full justify-center rounded-md text-center",
@ -390,6 +404,7 @@ export default function Signup({
}
if (!formMethods.getValues("email")) {
formMethods.trigger("email");
return;
}
const username = formMethods.getValues("username");
@ -410,17 +425,20 @@ export default function Signup({
)}
</div>
{/* Already have an account & T&C */}
<div className="mt-6">
<div className="mt-10 flex h-full flex-col justify-end text-xs">
<div className="flex flex-col text-sm">
<Link href="/auth/login" className="text-emphasis hover:underline">
{t("already_have_account")}
</Link>
<div className="flex gap-1">
<p className="text-subtle">{t("already_have_account")}</p>
<Link href="/auth/login" className="text-emphasis hover:underline">
{t("sign_in")}
</Link>
</div>
<div className="text-subtle">
By signing up, you agree to our{" "}
<Link className="text-emphasis hover:underline" href={`${WEBSITE_URL}/terms`}>
Terms of Service{" "}
Terms{" "}
</Link>
<span>and</span>{" "}
<span>&</span>{" "}
<Link className="text-emphasis hover:underline" href={`${WEBSITE_URL}/privacy`}>
Privacy Policy.
</Link>
@ -428,7 +446,12 @@ export default function Signup({
</div>
</div>
</div>
<div className="bg-subtle border-subtle hidden w-full flex-col justify-between rounded-l-2xl py-12 pl-12 lg:flex">
<div
className="border-subtle hidden w-full flex-col justify-between rounded-l-2xl border py-12 pl-12 lg:flex"
style={{
background:
"radial-gradient(162.05% 170% at 109.58% 35%, rgba(102, 117, 147, 0.7) 0%, rgba(212, 212, 213, 0.4) 100%) ",
}}>
{IS_CALCOM && (
<div className="mb-12 mr-12 grid h-full w-full grid-cols-4 gap-4 ">
<div className="">
@ -443,7 +466,7 @@ export default function Signup({
</div>
)}
<div
className="rounded-2xl border-y border-l border-dashed border-[#D1D5DB5A] py-[6px] pl-[6px]"
className="border-default rounded-bl-2xl rounded-br-none rounded-tl-2xl border-dashed py-[6px] pl-[6px]"
style={{
backgroundColor: "rgba(236,237,239,0.9)",
}}>

View File

@ -48,8 +48,6 @@ test.describe("Organization", () => {
await newPage.waitForLoadState("networkidle");
// Check required fields
await newPage.locator("button[type=submit]").click();
await expect(newPage.locator(".text-red-700")).toHaveCount(3); // 3 password hints
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await newPage.locator("button[type=submit]").click();
await newPage.waitForURL("/getting-started?from=signup");
@ -78,8 +76,8 @@ test.describe("Organization", () => {
await inviteLinkPage.waitForLoadState("networkidle");
// Check required fields
await inviteLinkPage.locator("button[type=submit]").click();
await expect(inviteLinkPage.locator(".text-red-700")).toHaveCount(4); // email + 3 password hints
const button = inviteLinkPage.locator("button[type=submit][disabled]");
await expect(button).toBeVisible(); // email + 3 password hints
// Happy path
await inviteLinkPage.locator("input[name=email]").fill(`rick@domain-${Date.now()}.com`);

View File

@ -0,0 +1,33 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.describe("Settings/admin A/B tests", () => {
test("should point to the /future/settings/admin page", async ({ page, users, context }) => {
await context.addCookies([
{
name: "x-calcom-future-routes-override",
value: "1",
url: "http://localhost:3000",
},
]);
const user = await users.create();
await user.apiLogin();
await page.goto("/settings/admin");
await page.waitForLoadState();
const dataNextJsRouter = await page.evaluate(() =>
window.document.documentElement.getAttribute("data-nextjs-router")
);
expect(dataNextJsRouter).toEqual("app");
const locator = page.getByRole("heading", { name: "Feature Flags" });
await expect(locator).toBeVisible();
});
});

View File

@ -51,8 +51,10 @@ test.describe("Team", () => {
await newPage.waitForLoadState("networkidle");
// Check required fields
await newPage.locator("button[type=submit]").click();
await expect(newPage.locator('[data-testid="hint-error"]')).toHaveCount(3);
const button = newPage.locator("button[type=submit][disabled]");
await expect(button).toBeVisible(); // email + 3 password hints
// Check required fields
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await newPage.locator("button[type=submit]").click();
await newPage.waitForURL("/getting-started?from=signup");

View File

@ -1581,6 +1581,7 @@
"enable_apps": "تمكين التطبيقات",
"enable_apps_description": "تمكين التطبيقات التي يمكن للمستخدمين دمجها مع {{appName}}",
"purchase_license": "شراء ترخيص",
"already_have_account": "هل لديك حساب بالفعل؟",
"already_have_key": "لدي مفتاح بالفعل:",
"already_have_key_suggestion": "يرجى نسخ متغير البيئة الحالي CALCOM_LICENSE_KEY هنا.",
"app_is_enabled": "تم تمكين {{appName}}",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Povolit aplikace",
"enable_apps_description": "Zapněte aplikace, které mohou uživatelé integrovat s aplikací {{appName}}",
"purchase_license": "Kupte si licenci",
"already_have_account": "Už máte účet?",
"already_have_key": "Klíč již mám:",
"already_have_key_suggestion": "Zkopírujte sem svou stávající proměnnou prostředí CALCOM_LICENSE_KEY.",
"app_is_enabled": "Aplikace {{appName}} je povolena",

View File

@ -1367,6 +1367,7 @@
"disabled_calendar": "Hvis du har en anden kalender installeret vil nye bookinger blive tilføjet til den. Hvis ikke, så forbind en ny kalender, så du ikke går glip af nye bookinger.",
"enable_apps": "Aktivér Apps",
"purchase_license": "Køb en licens",
"already_have_account": "Har du allerede en konto?",
"already_have_key": "Jeg har allerede en nøgle:",
"already_have_key_suggestion": "Kopiér venligst din eksisterende CALCOM_LICENSE_KEY environment variabel her.",
"app_is_enabled": "{{appName}} er aktiveret",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Apps aktivieren",
"enable_apps_description": "Apps aktivieren, die Benutzer mit {{appName}} integrieren können",
"purchase_license": "Lizenz kaufen",
"already_have_account": "Haben Sie bereits ein Konto?",
"already_have_key": "Ich habe bereits einen Schlüssel:",
"already_have_key_suggestion": "Bitte fügen Sie Ihre vorhandene CALCOM_LICENSE_KEY Umgebungsvariable hier ein.",
"app_is_enabled": "{{appName}} ist aktiviert",

View File

@ -274,5 +274,6 @@
"event_name_tooltip": "Το όνομα που θα εμφανίζεται στα ημερολόγια",
"label": "Ετικέτα",
"edit": "Επεξεργασία",
"disable_guests": "Απενεργοποίηση επισκεπτών"
"disable_guests": "Απενεργοποίηση επισκεπτών",
"already_have_account": "Έχετε ήδη λογαριασμό;"
}

View File

@ -78,7 +78,7 @@
"cannot_repackage_codebase": "You can not repackage or sell the codebase",
"acquire_license": "Acquire a commercial license to remove these terms by emailing",
"terms_summary": "Summary of terms",
"signing_up_terms":"By signing up, you agree to our <2>Terms of Service</2> and <3>Privacy Policy</3>.",
"signing_up_terms":"By signing up you agree to our <2>Terms</2> & <3>Privacy Policy</3>.",
"open_env": "Open .env and agree to our License",
"env_changed": "I've changed my .env",
"accept_license": "Accept License",
@ -673,6 +673,7 @@
"default_duration": "Default duration",
"default_duration_no_options": "Please choose available durations first",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minutes",
"round_robin": "Round Robin",
"round_robin_description": "Cycle meetings between multiple team members.",
@ -1480,6 +1481,8 @@
"report_app": "Report app",
"limit_booking_frequency": "Limit booking frequency",
"limit_booking_frequency_description": "Limit how many times this event can be booked",
"limit_booking_only_first_slot": "Limit booking only first slot",
"limit_booking_only_first_slot_description": "Allow only the first slot of every day to be booked",
"limit_total_booking_duration": "Limit total booking duration",
"limit_total_booking_duration_description": "Limit total amount of time that this event can be booked",
"add_limit": "Add Limit",
@ -1607,7 +1610,7 @@
"enable_apps": "Enable Apps",
"enable_apps_description": "Enable apps that users can integrate with {{appName}}",
"purchase_license": "Purchase a License",
"already_have_account":"I already have an account",
"already_have_account":"Already have an account?",
"already_have_key": "I already have a key:",
"already_have_key_suggestion": "Please copy your existing CALCOM_LICENSE_KEY environment variable here.",
"app_is_enabled": "{{appName}} is enabled",
@ -1865,6 +1868,7 @@
"looking_for_more_analytics": "Looking for more analytics?",
"looking_for_more_insights": "Looking for more Insights?",
"add_filter": "Add filter",
"remove_filters": "Clear all filters",
"select_user": "Select User",
"select_event_type": "Select Event Type",
"select_date_range": "Select Date Range",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Activar aplicaciones",
"enable_apps_description": "Habilite las aplicaciones que los usuarios pueden integrar con {{appName}}",
"purchase_license": "Compre una licencia",
"already_have_account": "¿Ya tienes una cuenta?",
"already_have_key": "Ya tengo una clave:",
"already_have_key_suggestion": "Copie aquí su variable de entorno CALCOM_LICENSE_KEY existente.",
"app_is_enabled": "{{appName}} está activada",

View File

@ -752,6 +752,7 @@
"enter_email_or_username": "Sartu email edo erabiltzaile izen bat",
"team_name_taken": "Izen hau dagoeneko hartua dago",
"must_enter_team_name": "Taldearentzat izen bat behar da",
"already_have_account": "Baduzu kontua dagoeneko?",
"fill_this_field": "Mesedez, bete ezazu eremu hau",
"options": "Aukerak",
"add_an_option": "Gehitu aukera bat",

View File

@ -660,6 +660,7 @@
"default_duration": "Durée par défaut",
"default_duration_no_options": "Veuillez d'abord choisir les durées disponibles",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "minutes",
"round_robin": "Round-robin",
"round_robin_description": "Alternez vos rendez-vous entre plusieurs membres d'équipe.",
@ -1843,6 +1844,7 @@
"looking_for_more_analytics": "Vous cherchez plus d'analyses ?",
"looking_for_more_insights": "Vous cherchez plus de statistiques ?",
"add_filter": "Ajouter un filtre",
"remove_filters": "Effacer les filtres",
"select_user": "Sélectionner un utilisateur",
"select_event_type": "Sélectionner un type d'événement",
"select_date_range": "Sélectionner une plage de dates",
@ -2085,11 +2087,17 @@
"overlay_my_calendar": "Superposer mon calendrier",
"overlay_my_calendar_toc": "En vous connectant à votre calendrier, vous acceptez notre politique de confidentialité et nos conditions d'utilisation. Vous pouvez révoquer cet accès à tout moment.",
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
"calendars_were_checking_for_conflicts": "Calendriers dont nous vérifions les conflits",
"availabilty_schedules": "Horaires de disponibilité",
"manage_calendars": "Gérer les calendriers",
"manage_availability_schedules": "Gérer les horaires de disponibilité",
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
"number_in_international_format": "Veuillez entrer le numéro au format international.",
"extensive_whitelabeling": "Marque blanche étendue",
"unlimited_teams": "Équipes illimitées",
"troubleshooter_tooltip": "Ouvrez l'outil de dépannage et déterminez ce qui ne va pas avec votre planning",
"need_help": "Besoin d'aide ?",
"troubleshooter": "Dépannage",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -1581,6 +1581,7 @@
"enable_apps": "הפעלת אפליקציות",
"enable_apps_description": "הפעל/הפעילי אפליקציות שמשתמשים יוכלו לשלב עם {{appName}}",
"purchase_license": "רכוש רישיון",
"already_have_account": "כבר יש לך חשבון?",
"already_have_key": "כבר יש לי מפתח:",
"already_have_key_suggestion": "אנא העתק את משתנה הסביבה CALCOM_LICENSE_KEY הקיים שלך לכאן.",
"app_is_enabled": "האפליקציה {{appName}} מופעלת",

View File

@ -333,5 +333,6 @@
"dark": "Tamna",
"automatically_adjust_theme": "Automatski prilagodite temu na temelju preferencija pozvanih osoba",
"user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije",
"full_name": "Puno ime"
"full_name": "Puno ime",
"already_have_account": "Već imate račun?"
}

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Abilita app",
"enable_apps_description": "Abilita le app che gli utenti possono integrare con {{appName}}",
"purchase_license": "Acquista una licenza",
"already_have_account": "Hai già un account?",
"already_have_key": "Ho già una chiave:",
"already_have_key_suggestion": "Copia qui la variabile di ambiente CALCOM_LICENSE_KEY esistente.",
"app_is_enabled": "{{appName}} è abilitato",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "アプリを有効にする",
"enable_apps_description": "ユーザーが {{appName}} と連携できるアプリを有効にします",
"purchase_license": "ライセンスを購入",
"already_have_account": "既にアカウントをお持ちですか?",
"already_have_key": "すでにキーを持っています:",
"already_have_key_suggestion": "ここに既存の CALCOM_LICENSE_KEY 環境変数をコピーしてください。",
"app_is_enabled": "{{appName}} は有効です",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "앱 활성화",
"enable_apps_description": "사용자가 {{appName}} 앱과 통합할 수 있는 앱 활성화",
"purchase_license": "라이선스 구매",
"already_have_account": "이미 계정이 있으신가요?",
"already_have_key": "이미 키가 있습니다:",
"already_have_key_suggestion": "기존 CALCOM_LICENSE_KEY 환경 변수를 여기에 복사하십시오.",
"app_is_enabled": "{{appName}} 앱이 활성화되었습니다",

View File

@ -124,5 +124,6 @@
"already_have_an_account": "Vai jums jau ir konts?",
"create_account": "Izveidot Kontu",
"confirm_password": "Apstiprināt paroli",
"confirm_auth_change": "Šis mainīs veidu, kā jūs autorizējaties"
"confirm_auth_change": "Šis mainīs veidu, kā jūs autorizējaties",
"already_have_account": "Vai jums jau ir konts?"
}

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Apps inschakelen",
"enable_apps_description": "Schakel apps in die gebruikers kunnen integreren met {{appName}}",
"purchase_license": "Koop een licentie",
"already_have_account": "Heeft u al een account?",
"already_have_key": "Ik heb al een code:",
"already_have_key_suggestion": "Kopieer hier uw bestaande CALCOM_LICENSE_KEY-omgevingsvariabele.",
"app_is_enabled": "{{appName}} is ingeschakeld",

View File

@ -1338,6 +1338,7 @@
"app_disabled_subject": "{{appName}} har blitt deaktivert",
"navigate_installed_apps": "Gå til installerte apper",
"enable_apps": "Aktiver Apper",
"already_have_account": "Har du allerede en bruker?",
"app_is_enabled": "{{appName}} er aktivert",
"app_is_disabled": "{{appName}} er deaktivert",
"disable_app": "Deaktiver App",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Włącz aplikacje",
"enable_apps_description": "Włącz aplikacje, które użytkownicy mogą zintegrować z aplikacją {{appName}}",
"purchase_license": "Kup licencję",
"already_have_account": "Masz już konto?",
"already_have_key": "Mam już klucz:",
"already_have_key_suggestion": "Skopiuj istniejącą zmienną środowiskową CALCOM_LICENSE_KEY tutaj.",
"app_is_enabled": "Aplikacja {{appName}} jest włączona",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Ativar aplicativos",
"enable_apps_description": "Ative aplicativos que os usuários podem integrar com o {{appName}}",
"purchase_license": "Comprar uma licença",
"already_have_account": "Já tem uma conta?",
"already_have_key": "Já tenho uma chave:",
"already_have_key_suggestion": "Copie a variável de ambiente CALCOM_LICENSE_KEY existente aqui.",
"app_is_enabled": "{{appName}} foi ativado",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Ativar aplicações",
"enable_apps_description": "Ativar aplicações que os utilizadores podem integrar com {{appName}}",
"purchase_license": "Adquira uma Licença",
"already_have_account": "Já tem uma conta?",
"already_have_key": "Eu já tenho uma chave:",
"already_have_key_suggestion": "Por favor, copie a sua variável de ambiente CALCOM_LICENSE_KEY existente aqui.",
"app_is_enabled": "{{appName}} foi ativada",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Activează aplicații",
"enable_apps_description": "Activați aplicațiile pe care utilizatorii le pot integra cu {{appName}}",
"purchase_license": "Achiziționați o licență",
"already_have_account": "Aveți deja un cont?",
"already_have_key": "Am deja o cheie:",
"already_have_key_suggestion": "Copiați aici variabila de mediu CALCOM_LICENSE_KEY existentă.",
"app_is_enabled": "{{appName}} este activat",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Включить приложения",
"enable_apps_description": "Выберите приложения, которые пользователи смогут интегрировать с {{appName}}",
"purchase_license": "Купить лицензию",
"already_have_account": "Уже есть аккаунт?",
"already_have_key": "У меня уже есть ключ:",
"already_have_key_suggestion": "Скопируйте сюда переменную окружения CALCOM_LICENSE_KEY.",
"app_is_enabled": "{{appName}} включено",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Omogući aplikacije",
"enable_apps_description": "Omogući aplikacije koje korisnici mogu da integrišu sa aplikacijom {{appName}}",
"purchase_license": "Kupovina licence",
"already_have_account": "Već imate nalog?",
"already_have_key": "Već imam ključ:",
"already_have_key_suggestion": "Kopirajte ovde postojeću CALCOM_LICENSE_KEY promenljivu okruženja.",
"app_is_enabled": "Aplikacija {{appName}} je omogućena",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Aktivera appar",
"enable_apps_description": "Aktivera appar som användare kan integrera med {{appName}}",
"purchase_license": "Köp en licens",
"already_have_account": "Har du redan ett konto?",
"already_have_key": "Jag har redan en nyckel:",
"already_have_key_suggestion": "Kopiera din befintliga CALCOM_LICENSE_KEY-miljövariabel här.",
"app_is_enabled": "{{appName}} har aktiverats",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Uygulamaları Etkinleştir",
"enable_apps_description": "Kullanıcıların {{appName}} ile entegre edebileceği uygulamaları etkinleştirin",
"purchase_license": "Lisans satın alın",
"already_have_account": "Zaten bir hesabınız var mı?",
"already_have_key": "Zaten bir anahtarım var:",
"already_have_key_suggestion": "Lütfen mevcut CALCOM_LICENSE_KEY ortam değişkeninizi buraya kopyalayın.",
"app_is_enabled": "{{appName}} etkinleştirildi",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Увімкнути додатки",
"enable_apps_description": "Активуйте застосунки, які користувачі зможуть інтегрувати із застосунком {{appName}}",
"purchase_license": "Придбати ліцензію",
"already_have_account": "Уже маєте обліковий запис?",
"already_have_key": "У мене вже є ключ:",
"already_have_key_suggestion": "Скопіюйте сюди наявну змінну оточення CALCOM_LICENSE_KEY.",
"app_is_enabled": "{{appName}} увімкнено",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "Kích hoạt ứng dụng",
"enable_apps_description": "Kích hoạt những ứng dụng mà người dùng có thể tích hợp với {{appName}}",
"purchase_license": "Mua một giấy phép",
"already_have_account": "Bạn đã có sẵn tài khoản?",
"already_have_key": "Tôi đã có một khoá:",
"already_have_key_suggestion": "Vui lòng sao chép biến số môi trường CALCOM_LICENSE_KEY hiện có của bạn tại đây.",
"app_is_enabled": "{{appName}} đã được kích hoạt",

View File

@ -1582,6 +1582,7 @@
"enable_apps": "启用应用",
"enable_apps_description": "启用用户可以与 {{appName}} 集成的应用",
"purchase_license": "购买许可证",
"already_have_account": "已经有帐号?",
"already_have_key": "我已经有密钥:",
"already_have_key_suggestion": "请将您现有的 CALCOM_LICENSE_KEY 环境变量复制到此处。",
"app_is_enabled": "{{appName}} 已启用",

View File

@ -1581,6 +1581,7 @@
"enable_apps": "啟用應用程式",
"enable_apps_description": "啟用使用者可與 {{appName}} 整合的應用程式",
"purchase_license": "購買授權",
"already_have_account": "已經有帳號嗎?",
"already_have_key": "我已經有金鑰:",
"already_have_key_suggestion": "請複製您目前的 CALCOM_LICENSE_KEY 環境變數至此處。",
"app_is_enabled": "已啟用 {{appName}}",

View File

@ -2,4 +2,5 @@ import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
});

View File

@ -4,7 +4,7 @@ import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalend
import { parse } from "node-html-parser";
import type { VEvent } from "node-ical";
import ical from "node-ical";
import { expect } from "vitest";
import { expect, vi } from "vitest";
import "vitest-fetch-mock";
import dayjs from "@calcom/dayjs";
@ -547,7 +547,7 @@ export function expectCalendarEventCreationFailureEmails({
);
}
export function expectSuccessfulRoudRobinReschedulingEmails({
export function expectSuccessfulRoundRobinReschedulingEmails({
emails,
newOrganizer,
prevOrganizer,
@ -557,32 +557,38 @@ export function expectSuccessfulRoudRobinReschedulingEmails({
prevOrganizer: { email: string; name: string };
}) {
if (newOrganizer !== prevOrganizer) {
// new organizer should recieve scheduling emails
expect(emails).toHaveEmail(
{
heading: "new_event_scheduled",
to: `${newOrganizer.email}`,
},
`${newOrganizer.email}`
);
vi.waitFor(() => {
// new organizer should recieve scheduling emails
expect(emails).toHaveEmail(
{
heading: "new_event_scheduled",
to: `${newOrganizer.email}`,
},
`${newOrganizer.email}`
);
});
// old organizer should recieve cancelled emails
expect(emails).toHaveEmail(
{
heading: "event_request_cancelled",
to: `${prevOrganizer.email}`,
},
`${prevOrganizer.email}`
);
vi.waitFor(() => {
// old organizer should recieve cancelled emails
expect(emails).toHaveEmail(
{
heading: "event_request_cancelled",
to: `${prevOrganizer.email}`,
},
`${prevOrganizer.email}`
);
});
} else {
// organizer should recieve rescheduled emails
expect(emails).toHaveEmail(
{
heading: "event_has_been_rescheduled",
to: `${newOrganizer.email}`,
},
`${newOrganizer.email}`
);
vi.waitFor(() => {
// organizer should recieve rescheduled emails
expect(emails).toHaveEmail(
{
heading: "event_has_been_rescheduled",
to: `${newOrganizer.email}`,
},
`${newOrganizer.email}`
);
});
}
}

View File

@ -100,7 +100,7 @@
"prismock": "^1.21.1",
"tsc-absolute": "^1.0.0",
"typescript": "^4.9.4",
"vitest": "^0.34.3",
"vitest": "^0.34.6",
"vitest-fetch-mock": "^0.2.2",
"vitest-mock-extended": "^1.1.3"
},

View File

@ -27,6 +27,7 @@ export const EventTypeAddonMap = {
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")),
matomo: dynamic(() => import("./matomo/components/EventTypeAppCardInterface")),
metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")),
paypal: dynamic(() => import("./paypal/components/EventTypeAppCardInterface")),
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),

View File

@ -15,6 +15,7 @@ import { appKeysSchema as intercom_zod_ts } from "./intercom/zod";
import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appKeysSchema as make_zod_ts } from "./make/zod";
import { appKeysSchema as matomo_zod_ts } from "./matomo/zod";
import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod";
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
@ -51,6 +52,7 @@ export const appKeysSchemas = {
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
make: make_zod_ts,
matomo: matomo_zod_ts,
metapixel: metapixel_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,

View File

@ -33,6 +33,7 @@ import intercom_config_json from "./intercom/config.json";
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
import make_config_json from "./make/config.json";
import matomo_config_json from "./matomo/config.json";
import metapixel_config_json from "./metapixel/config.json";
import mirotalk_config_json from "./mirotalk/config.json";
import n8n_config_json from "./n8n/config.json";
@ -109,6 +110,7 @@ export const appStoreMetadata = {
jitsivideo: jitsivideo__metadata_ts,
larkcalendar: larkcalendar__metadata_ts,
make: make_config_json,
matomo: matomo_config_json,
metapixel: metapixel_config_json,
mirotalk: mirotalk_config_json,
n8n: n8n_config_json,

View File

@ -15,6 +15,7 @@ import { appDataSchema as intercom_zod_ts } from "./intercom/zod";
import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appDataSchema as make_zod_ts } from "./make/zod";
import { appDataSchema as matomo_zod_ts } from "./matomo/zod";
import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod";
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
@ -51,6 +52,7 @@ export const appDataSchemas = {
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
make: make_zod_ts,
matomo: matomo_zod_ts,
metapixel: metapixel_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,

View File

@ -33,6 +33,7 @@ export const apiHandlers = {
jitsivideo: import("./jitsivideo/api"),
larkcalendar: import("./larkcalendar/api"),
make: import("./make/api"),
matomo: import("./matomo/api"),
metapixel: import("./metapixel/api"),
mirotalk: import("./mirotalk/api"),
n8n: import("./n8n/api"),

View File

@ -15,6 +15,7 @@ import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
import gtm_config_json from "./gtm/config.json";
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
import matomo_config_json from "./matomo/config.json";
import metapixel_config_json from "./metapixel/config.json";
import mirotalk_config_json from "./mirotalk/config.json";
import office365video_config_json from "./office365video/config.json";
@ -48,6 +49,7 @@ export const appStoreMetadata = {
gtm: gtm_config_json,
huddle01video: huddle01video__metadata_ts,
jitsivideo: jitsivideo__metadata_ts,
matomo: matomo_config_json,
metapixel: metapixel_config_json,
mirotalk: mirotalk_config_json,
office365video: office365video_config_json,

View File

@ -0,0 +1,6 @@
---
items:
- 1.png
---
{DESCRIPTION}

View File

@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};
export default handler;

View File

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

View File

@ -0,0 +1,47 @@
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
const matomoUrl = getAppData("MATOMO_URL");
const siteId = getAppData("SITE_ID");
const { enabled, updateEnabled } = useIsAppEnabled(app);
return (
<AppCard
app={app}
switchOnClick={(e) => {
updateEnabled(e);
}}
switchChecked={enabled}
teamId={eventType.team?.id || undefined}>
<div className="flex flex-col gap-2">
<TextField
name="Matomo URL"
placeholder="Enter your Matomo URL here"
value={matomoUrl}
disabled={disabled}
onChange={(e) => {
setAppData("MATOMO_URL", e.target.value);
}}
/>
<TextField
disabled={disabled}
name="Site ID"
placeholder="Enter your Site ID"
value={siteId}
onChange={(e) => {
setAppData("SITE_ID", e.target.value);
}}
/>
</div>
</AppCard>
);
};
export default EventTypeAppCard;

View File

@ -0,0 +1,29 @@
{
"name": "Matomo",
"slug": "matomo",
"type": "matomo_analytics",
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "analytics",
"categories": ["analytics"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "Google Analytics alternative that protects your data and your customers' privacy",
"extendsFeature": "EventType",
"appData": {
"tag": {
"scripts": [
{
"src": "{MATOMO_URL}/matomo.js",
"attrs": {}
},
{
"content": "var _paq = window._paq || [];\n _paq.push(['trackPageView']);\n _paq.push(['enableLinkTracking']);\n (function() {\n var u='{MATOMO_URL}/'; \n _paq.push(['setTrackerUrl', u+'matomo.php']);\n _paq.push(['setSiteId', '{SITE_ID}']); \n var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];\n g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);\n })();"
}
]
}
},
"isTemplate": false,
"__createdUsingCli": true,
"__template": "booking-pages-tag"
}

View File

@ -0,0 +1 @@
export * as api from "./api";

View File

@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/matomo",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "Google Analytics alternative that protects your data and your customers' privacy"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,12 @@
import { z } from "zod";
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
export const appDataSchema = eventTypeAppCardZod.merge(
z.object({
MATOMO_URL: z.string().optional(),
SITE_ID: z.string().optional(),
})
);
export const appKeysSchema = z.object({});

View File

@ -25,14 +25,14 @@ export default class ResponseEmail extends BaseEmail {
this.toAddresses = toAddresses;
}
protected getNodeMailerPayload(): Record<string, unknown> {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const toAddresses = this.toAddresses;
const subject = `${this.form.name} has a new response`;
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject,
html: renderEmail("ResponseEmail", {
html: await renderEmail("ResponseEmail", {
form: this.form,
orderedResponses: this.orderedResponses,
subject,

View File

@ -1,3 +1,5 @@
"use client";
import { z } from "zod";
import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";

View File

@ -25,6 +25,7 @@ import type {
} from "@calcom/types/Calendar";
import { getBusyTimes, getBusyTimesForLimitChecks } from "./getBusyTimes";
import monitorCallbackAsync, { monitorCallbackSync } from "./sentryWrapper";
const log = logger.getSubLogger({ prefix: ["getUserAvailability"] });
const availabilitySchema = z
@ -41,7 +42,13 @@ const availabilitySchema = z
})
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
const getEventType = async (id: number) => {
const getEventType = async (
...args: Parameters<typeof _getEventType>
): Promise<ReturnType<typeof _getEventType>> => {
return monitorCallbackAsync(_getEventType, ...args);
};
const _getEventType = async (id: number) => {
const eventType = await prisma.eventType.findUnique({
where: { id },
select: {
@ -86,7 +93,11 @@ const getEventType = async (id: number) => {
type EventType = Awaited<ReturnType<typeof getEventType>>;
const getUser = (where: Prisma.UserWhereInput) =>
const getUser = (...args: Parameters<typeof _getUser>): ReturnType<typeof _getUser> => {
return monitorCallbackSync(_getUser, ...args);
};
const _getUser = (where: Prisma.UserWhereInput) =>
prisma.user.findFirst({
where,
select: {
@ -99,7 +110,13 @@ const getUser = (where: Prisma.UserWhereInput) =>
type User = Awaited<ReturnType<typeof getUser>>;
export const getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) =>
export const getCurrentSeats = (
...args: Parameters<typeof _getCurrentSeats>
): ReturnType<typeof _getCurrentSeats> => {
return monitorCallbackSync(_getCurrentSeats, ...args);
};
const _getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) =>
prisma.booking.findMany({
where: {
eventTypeId,
@ -122,8 +139,14 @@ export const getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Da
export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
export const getUserAvailability = async (
...args: Parameters<typeof _getUserAvailability>
): Promise<ReturnType<typeof _getUserAvailability>> => {
return monitorCallbackAsync(_getUserAvailability, ...args);
};
/** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */
export const getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseAndEverythingElse(
const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseAndEverythingElse(
query: {
withSource?: boolean;
username?: string;
@ -305,7 +328,13 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
};
};
const getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => {
const getPeriodStartDatesBetween = (
...args: Parameters<typeof _getPeriodStartDatesBetween>
): ReturnType<typeof _getPeriodStartDatesBetween> => {
return monitorCallbackSync(_getPeriodStartDatesBetween, ...args);
};
const _getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => {
const dates = [];
let startDate = dayjs(dateFrom).startOf(period);
const endDate = dayjs(dateTo).endOf(period);
@ -378,6 +407,12 @@ class LimitManager {
}
const getBusyTimesFromLimits = async (
...args: Parameters<typeof _getBusyTimesFromLimits>
): Promise<ReturnType<typeof _getBusyTimesFromLimits>> => {
return monitorCallbackAsync(_getBusyTimesFromLimits, ...args);
};
const _getBusyTimesFromLimits = async (
bookingLimits: IntervalLimit | null,
durationLimits: IntervalLimit | null,
dateFrom: Dayjs,
@ -450,6 +485,12 @@ const getBusyTimesFromLimits = async (
};
const getBusyTimesFromBookingLimits = async (
...args: Parameters<typeof _getBusyTimesFromBookingLimits>
): Promise<ReturnType<typeof _getBusyTimesFromBookingLimits>> => {
return monitorCallbackAsync(_getBusyTimesFromBookingLimits, ...args);
};
const _getBusyTimesFromBookingLimits = async (
bookings: EventBusyDetails[],
bookingLimits: IntervalLimit,
dateFrom: Dayjs,
@ -504,6 +545,12 @@ const getBusyTimesFromBookingLimits = async (
};
const getBusyTimesFromDurationLimits = async (
...args: Parameters<typeof _getBusyTimesFromDurationLimits>
): Promise<ReturnType<typeof _getBusyTimesFromDurationLimits>> => {
return monitorCallbackAsync(_getBusyTimesFromDurationLimits, ...args);
};
const _getBusyTimesFromDurationLimits = async (
bookings: EventBusyDetails[],
durationLimits: IntervalLimit,
dateFrom: Dayjs,

View File

@ -0,0 +1,86 @@
import * as Sentry from "@sentry/nextjs";
import type { Span, Transaction } from "@sentry/types";
/*
WHEN TO USE
We ran a script that performs a simple mathematical calculation within a loop of 1000000 iterations.
Our results were: Plain execution time: 441, Monitored execution time: 8094.
This suggests that using these wrappers within large loops can incur significant overhead and is thus not recommended.
For smaller loops, the cost incurred may not be very significant on an absolute scale
considering that a million monitored iterations only took roughly 8 seconds when monitored.
*/
const setUpMonitoring = (name: string) => {
// Attempt to retrieve the current transaction from Sentry's scope
let transaction = Sentry.getCurrentHub().getScope()?.getTransaction();
// Check if there's an existing transaction, if not, start a new one
if (!transaction) {
transaction = Sentry.startTransaction({
op: name,
name: name,
});
}
// Start a new span in the current transaction
const span = transaction.startChild({
op: name,
description: `Executing ${name}`,
});
return [transaction, span];
};
// transaction will always be Transaction, since returned in a list with Span type must be listed as either or here
const finishMonitoring = (transaction: Transaction | Span, span: Span) => {
// Attempt to retrieve the current transaction from Sentry's scope
span.finish();
// If this was a new transaction, finish it
if (!Sentry.getCurrentHub().getScope()?.getTransaction()) {
transaction.finish();
}
};
const monitorCallbackAsync = async <T extends (...args: any[]) => any>(
cb: T,
...args: Parameters<T>
): Promise<ReturnType<T>> => {
// Check if Sentry set
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return (await cb(...args)) as ReturnType<T>;
const [transaction, span] = setUpMonitoring(cb.name);
try {
const result = await cb(...args);
return result as ReturnType<T>;
} catch (error) {
Sentry.captureException(error);
throw error;
} finally {
finishMonitoring(transaction, span);
}
};
const monitorCallbackSync = <T extends (...args: any[]) => any>(
cb: T,
...args: Parameters<T>
): ReturnType<T> => {
// Check if Sentry set
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return cb(...args) as ReturnType<T>;
const [transaction, span] = setUpMonitoring(cb.name);
try {
const result = cb(...args);
return result as ReturnType<T>;
} catch (error) {
Sentry.captureException(error);
throw error;
} finally {
finishMonitoring(transaction, span);
}
};
export default monitorCallbackAsync;
export { monitorCallbackSync };

View File

@ -8,7 +8,7 @@
```ts
import { renderEmail } from "@calcom/emails";
renderEmail("TeamInviteEmail", */{
renderEmail("TeamInviteEmail", {
language: t,
from: "teampro@example.com",
to: "pro@example.com",

View File

@ -1,12 +1,11 @@
import * as ReactDOMServer from "react-dom/server";
import * as templates from "./templates";
function renderEmail<K extends keyof typeof templates>(
async function renderEmail<K extends keyof typeof templates>(
template: K,
props: React.ComponentProps<(typeof templates)[K]>
) {
const Component = templates[template];
const ReactDOMServer = (await import("react-dom/server")).default;
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error

View File

@ -24,7 +24,7 @@ export default class BaseEmail {
return dayjs(time).tz(this.getTimezone()).locale(this.getLocale()).format(format);
}
protected getNodeMailerPayload(): Record<string, unknown> {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {};
}
public async sendEmail() {
@ -38,21 +38,20 @@ export default class BaseEmail {
if (process.env.INTEGRATION_TEST_MODE === "true") {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
setTestEmail(this.getNodeMailerPayload());
setTestEmail(await this.getNodeMailerPayload());
console.log(
"Skipped Sending Email as process.env.NEXT_PUBLIC_UNIT_TESTS is set. Emails are available in globalThis.testEmails"
);
return new Promise((r) => r("Skipped sendEmail for Unit Tests"));
}
const payload = this.getNodeMailerPayload();
const payload = await this.getNodeMailerPayload();
const parseSubject = z.string().safeParse(payload?.subject);
const payloadWithUnEscapedSubject = {
headers: this.getMailerOptions().headers,
...payload,
...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }),
};
await new Promise((resolve, reject) =>
createTransport(this.getMailerOptions().transport).sendMail(
payloadWithUnEscapedSubject,
@ -69,7 +68,6 @@ export default class BaseEmail {
).catch((e) => console.error("sendEmail", e));
return new Promise((resolve) => resolve("send mail async"));
}
protected getMailerOptions() {
return {
transport: serverConfig.transport,
@ -77,7 +75,6 @@ export default class BaseEmail {
headers: serverConfig.headers,
};
}
protected printNodeMailerError(error: Error): void {
/** Don't clog the logs with unsent emails in E2E */
if (process.env.NEXT_PUBLIC_IS_E2E) return;

View File

@ -23,14 +23,14 @@ export default class AccountVerifyEmail extends BaseEmail {
this.verifyAccountInput = passwordEvent;
}
protected getNodeMailerPayload(): Record<string, unknown> {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`,
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
subject: this.verifyAccountInput.language("verify_email_subject", {
appName: APP_NAME,
}),
html: renderEmail("VerifyAccountEmail", this.verifyAccountInput),
html: await renderEmail("VerifyAccountEmail", this.verifyAccountInput),
text: this.getTextBody(),
};
}

View File

@ -22,12 +22,12 @@ export default class AdminOrganizationNotification extends BaseEmail {
this.input = input;
}
protected getNodeMailerPayload(): Record<string, unknown> {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: this.input.instanceAdmins.map((admin) => admin.email).join(","),
subject: `${this.input.t("admin_org_notification_email_subject")}`,
html: renderEmail("AdminOrganizationNotificationEmail", {
html: await renderEmail("AdminOrganizationNotificationEmail", {
orgSlug: this.input.orgSlug,
webappIPAddress: this.input.webappIPAddress,
language: this.input.t,

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