diff --git a/.yarn/versions/6e890e70.yml b/.yarn/versions/6e890e70.yml
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/.yarn/versions/d1e07a23.yml b/.yarn/versions/d1e07a23.yml
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/apps/web/lib/plain/plainChat.tsx b/apps/web/lib/plain/plainChat.tsx
index d80699c3638f09..f3bba3fdf5eae9 100644
--- a/apps/web/lib/plain/plainChat.tsx
+++ b/apps/web/lib/plain/plainChat.tsx
@@ -79,11 +79,16 @@ const PlainChat = () => {
const userEmail = session?.user?.email;
const isAppDomain = useMemo(() => {
- const restrictedPaths = process.env.NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS?.split(",") || [];
+ const restrictedPathsSet = new Set(
+ (process.env.NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS?.split(",") || []).map((path) => path.trim())
+ );
+
+ const pathSegments = pathname?.split("/").filter(Boolean) || [];
+
return (
typeof window !== "undefined" &&
window.location.origin === process.env.NEXT_PUBLIC_WEBAPP_URL &&
- !restrictedPaths.some((path) => pathname?.startsWith(path.trim()))
+ !pathSegments.some((segment) => restrictedPathsSet.has(segment))
);
}, [pathname]);
diff --git a/apps/web/package.json b/apps/web/package.json
index 8e672d2b7d3780..615518fbee669f 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
- "version": "4.8.9",
+ "version": "4.8.11",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts
index 7cc89f19f3172f..2b5b199e61856a 100644
--- a/apps/web/playwright/lib/testUtils.ts
+++ b/apps/web/playwright/lib/testUtils.ts
@@ -1,4 +1,4 @@
-import type { Frame, Locator, Page, Request as PlaywrightRequest } from "@playwright/test";
+import type { Frame, Page, Request as PlaywrightRequest } from "@playwright/test";
import { expect } from "@playwright/test";
import { createHash } from "crypto";
import EventEmitter from "events";
@@ -538,31 +538,3 @@ export async function expectPageToBeNotFound({ page, url }: { page: Page; url: s
await page.goto(`${url}`);
await expect(page.getByTestId(`404-page`)).toBeVisible();
}
-
-export async function clickUntilDialogVisible(
- dialogOpenButton: Locator,
- visibleLocatorOnDialog: Locator,
- page: Page,
- matchUrl: string,
- retries = 3,
- delay = 2000
-) {
- for (let i = 0; i < retries; i++) {
- try {
- const responsePromise = page.waitForResponse(
- (response) => response.url().includes(matchUrl) && response.status() === 200
- );
- await dialogOpenButton.click();
- await responsePromise;
- await visibleLocatorOnDialog.waitFor({ state: "visible", timeout: delay });
- return;
- } catch {
- console.warn(`clickUntilDialogVisible: Attempt ${i + 1} failed to open dialog`);
- if (i === retries - 1) {
- console.log("clickUntilDialogVisible: Dialog did not appear after multiple attempts.");
- return;
- }
- await new Promise((resolve) => setTimeout(resolve, delay));
- }
- }
-}
diff --git a/apps/web/playwright/onboarding.e2e.ts b/apps/web/playwright/onboarding.e2e.ts
index 94aecd6faca589..dd93e58ff57e3c 100644
--- a/apps/web/playwright/onboarding.e2e.ts
+++ b/apps/web/playwright/onboarding.e2e.ts
@@ -1,93 +1,86 @@
-/* eslint-disable playwright/no-skipped-test */
import { expect } from "@playwright/test";
import { IdentityProvider } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
-test.describe.configure({ mode: "serial" });
+test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test.describe("Onboarding", () => {
- test.describe("Onboarding v2", () => {
- const testOnboarding = (identityProvider: IdentityProvider) => {
- test(`Onboarding Flow - ${identityProvider} user`, async ({ page, users }) => {
- const user = await users.create({
- completedOnboarding: false,
- name: null,
- identityProvider,
- });
- await user.apiLogin();
- await page.goto("/getting-started");
- // tests whether the user makes it to /getting-started
- // after login with completedOnboarding false
- await page.waitForURL("/getting-started");
-
- await test.step("step 1 - User Settings", async () => {
- // Check required fields
- await page.locator("button[type=submit]").click();
- await expect(page.locator("data-testid=required")).toBeVisible();
-
- // happy path
- await page.locator("input[name=username]").fill("new user onboarding");
- await page.locator("input[name=name]").fill("new user 2");
- await page.locator("input[role=combobox]").click();
- await page
- .locator("*")
- .filter({ hasText: /^Europe\/London/ })
- .first()
- .click();
- await page.locator("button[type=submit]").click();
-
- await expect(page).toHaveURL(/.*connected-calendar/);
-
- const userComplete = await user.self();
- expect(userComplete.name).toBe("new user 2");
- });
-
- await test.step("step 2 - Connected Calendar", async () => {
- const isDisabled = await page.locator("button[data-testid=save-calendar-button]").isDisabled();
- await expect(isDisabled).toBe(true);
- // tests skip button, we don't want to test entire flow.
- await page.locator("button[data-testid=skip-step]").click();
-
- await expect(page).toHaveURL(/.*connected-video/);
- });
-
- await test.step("step 3 - Connected Video", async () => {
- const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled();
- await expect(isDisabled).toBe(true);
- // tests skip button, we don't want to test entire flow.
- await page.locator("button[data-testid=skip-step]").click();
-
- await expect(page).toHaveURL(/.*setup-availability/);
- });
-
- await test.step("step 4 - Setup Availability", async () => {
- const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
- await expect(isDisabled).toBe(false);
- // same here, skip this step.
- await page.locator("button[data-testid=save-availability]").click();
-
- await expect(page).toHaveURL(/.*user-profile/);
- });
-
- await test.step("step 5- User Profile", async () => {
- await page.locator("button[type=submit]").click();
-
- // should redirect to /event-types after onboarding
- await page.waitForURL("/event-types");
-
- const userComplete = await user.self();
-
- expect(userComplete.bio?.replace("
", "").length).toBe(0);
- });
+ const testOnboarding = (identityProvider: IdentityProvider) => {
+ test(`Onboarding Flow - ${identityProvider} user`, async ({ page, users }) => {
+ const user = await users.create({
+ completedOnboarding: false,
+ name: null,
+ identityProvider,
});
- };
+ await user.apiLogin();
+ await page.goto("/getting-started");
+ // tests whether the user makes it to /getting-started
+ // after login with completedOnboarding false
+ await page.waitForURL("/getting-started");
+
+ await test.step("step 1 - User Settings", async () => {
+ // Check required fields
+ await page.locator("button[type=submit]").click();
+ await expect(page.locator("data-testid=required")).toBeVisible();
+
+ // happy path
+ await page.locator("input[name=username]").fill("new user onboarding");
+ await page.locator("input[name=name]").fill("new user 2");
+ await page.locator("input[role=combobox]").click();
+ await page
+ .locator("*")
+ .filter({ hasText: /^Europe\/London/ })
+ .first()
+ .click();
+ await page.locator("button[type=submit]").click();
+
+ await expect(page).toHaveURL(/.*connected-calendar/);
+
+ const userComplete = await user.self();
+ expect(userComplete.name).toBe("new user 2");
+ });
+
+ await test.step("step 2 - Connected Calendar", async () => {
+ const isDisabled = await page.locator("button[data-testid=save-calendar-button]").isDisabled();
+ await expect(isDisabled).toBe(true);
+ // tests skip button, we don't want to test entire flow.
+ await page.locator("button[data-testid=skip-step]").click();
+ await expect(page).toHaveURL(/.*connected-video/);
+ });
+
+ await test.step("step 3 - Connected Video", async () => {
+ const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled();
+ await expect(isDisabled).toBe(true);
+ // tests skip button, we don't want to test entire flow.
+ await page.locator("button[data-testid=skip-step]").click();
+ await expect(page).toHaveURL(/.*setup-availability/);
+ });
+
+ await test.step("step 4 - Setup Availability", async () => {
+ const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
+ await expect(isDisabled).toBe(false);
+ // same here, skip this step.
+
+ await page.locator("button[data-testid=save-availability]").click();
+ await expect(page).toHaveURL(/.*user-profile/);
+ });
+
+ await test.step("step 5- User Profile", async () => {
+ await page.locator("button[type=submit]").click();
+ // should redirect to /event-types after onboarding
+ await page.waitForURL("/event-types");
+
+ const userComplete = await user.self();
+ expect(userComplete.bio?.replace("
", "").length).toBe(0);
+ });
+ });
+ };
- testOnboarding(IdentityProvider.GOOGLE);
- testOnboarding(IdentityProvider.CAL);
- testOnboarding(IdentityProvider.SAML);
- });
+ testOnboarding(IdentityProvider.GOOGLE);
+ testOnboarding(IdentityProvider.CAL);
+ testOnboarding(IdentityProvider.SAML);
});
diff --git a/apps/web/playwright/out-of-office.e2e.ts b/apps/web/playwright/out-of-office.e2e.ts
index c251ccaef6689c..9152db47dba834 100644
--- a/apps/web/playwright/out-of-office.e2e.ts
+++ b/apps/web/playwright/out-of-office.e2e.ts
@@ -7,7 +7,7 @@ import { randomString } from "@calcom/lib/random";
import prisma from "@calcom/prisma";
import { test } from "./lib/fixtures";
-import { submitAndWaitForResponse, localize, clickUntilDialogVisible } from "./lib/testUtils";
+import { submitAndWaitForResponse, localize } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(async ({ users }) => {
@@ -20,12 +20,18 @@ test.describe("Out of office", () => {
await user.apiLogin();
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
await page.goto("/settings/my-account/out-of-office");
await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
- const addOOOButton = page.getByTestId("add_entry_ooo");
- const dateButton = page.locator('[data-testid="date-range"]');
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
+ const reasonListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200
+ );
+ await page.getByTestId("add_entry_ooo").click();
+ await reasonListRespPromise;
await page.getByTestId("reason_select").click();
@@ -71,12 +77,18 @@ test.describe("Out of office", () => {
await user.apiLogin();
- await page.goto(`/settings/my-account/out-of-office`);
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
+ await page.goto("/settings/my-account/out-of-office");
await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
- const addOOOButton = page.getByTestId("add_entry_ooo");
- const dateButton = page.locator('[data-testid="date-range"]');
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
+ const reasonListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200
+ );
+ await page.getByTestId("add_entry_ooo").click();
+ await reasonListRespPromise;
await page.getByTestId("reason_select").click();
@@ -152,7 +164,12 @@ test.describe("Out of office", () => {
await user.apiLogin();
- await page.goto(`/settings/my-account/out-of-office`);
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
+ await page.goto("/settings/my-account/out-of-office");
+ await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
// expect table-redirect-toUserId to be visible
await expect(page.locator(`data-testid=table-redirect-${userTo.username}`)).toBeVisible();
@@ -211,14 +228,20 @@ test.describe("Out of office", () => {
await user.apiLogin();
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
await page.goto("/settings/my-account/out-of-office");
await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
- const addOOOButton = page.getByTestId("add_entry_ooo");
- const dateButton = page.locator('[data-testid="date-range"]');
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
+ const reasonListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200
+ );
+ await page.getByTestId("add_entry_ooo").click();
+ await reasonListRespPromise;
- await dateButton.click();
+ await page.locator('[data-testid="date-range"]').click();
await selectToAndFromDates(page, "13", "22", true);
@@ -254,14 +277,20 @@ test.describe("Out of office", () => {
await user.apiLogin();
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
await page.goto("/settings/my-account/out-of-office");
await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
- const addOOOButton = page.getByTestId("add_entry_ooo");
- const dateButton = page.locator('[data-testid="date-range"]');
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
+ const reasonListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200
+ );
+ await page.getByTestId("add_entry_ooo").click();
+ await reasonListRespPromise;
- await dateButton.click();
+ await page.locator('[data-testid="date-range"]').click();
await selectToAndFromDates(page, "13", "22");
@@ -270,8 +299,11 @@ test.describe("Out of office", () => {
await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible();
// add another entry
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
- await dateButton.click();
+ await entriesListRespPromise;
+ await page.getByTestId("add_entry_ooo").click();
+ await reasonListRespPromise;
+
+ await page.locator('[data-testid="date-range"]').click();
await selectToAndFromDates(page, "11", "24");
@@ -286,14 +318,20 @@ test.describe("Out of office", () => {
await user.apiLogin();
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
await page.goto("/settings/my-account/out-of-office");
await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
- const addOOOButton = page.getByTestId("add_entry_ooo");
- const dateButton = page.locator('[data-testid="date-range"]');
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
+ const reasonListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200
+ );
+ await page.getByTestId("add_entry_ooo").click();
+ await reasonListRespPromise;
- await dateButton.click();
+ await page.locator('[data-testid="date-range"]').click();
await selectToAndFromDates(page, "13", "22");
@@ -302,8 +340,11 @@ test.describe("Out of office", () => {
await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible();
// add another entry
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
- await dateButton.click();
+ await entriesListRespPromise;
+ await page.getByTestId("add_entry_ooo").click();
+ await reasonListRespPromise;
+
+ await page.locator('[data-testid="date-range"]').click();
await selectToAndFromDates(page, "13", "22");
@@ -315,21 +356,31 @@ test.describe("Out of office", () => {
const user = await users.create({ name: "userOne" });
await user.apiLogin();
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
await page.goto("/settings/my-account/out-of-office");
- await page.waitForLoadState();
+ await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
- const addOOOButton = await page.getByTestId("add_entry_ooo");
- const dateButton = await page.locator('[data-testid="date-range"]');
+ const addOOOButton = page.getByTestId("add_entry_ooo");
+ const dateButton = page.locator('[data-testid="date-range"]');
+ const reasonListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200
+ );
+ await addOOOButton.click();
+ await reasonListRespPromise;
//Creates 2 OOO entries:
//First OOO is created on Next month 1st - 3rd
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
await dateButton.click();
await selectDateAndCreateOOO(page, "1", "3");
await expect(page.locator(`data-testid=table-redirect-n-a`).nth(0)).toBeVisible();
//Second OOO is created on Next month 4th - 6th
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
+ await entriesListRespPromise;
+ await addOOOButton.click();
+ await reasonListRespPromise;
await dateButton.click();
await selectDateAndCreateOOO(page, "4", "6");
await expect(page.locator(`data-testid=table-redirect-n-a`).nth(1)).toBeVisible();
@@ -349,14 +400,22 @@ test.describe("Out of office", () => {
await owner.apiLogin();
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
await page.goto("/settings/my-account/out-of-office");
- await page.waitForLoadState();
+ await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
- const addOOOButton = await page.getByTestId("add_entry_ooo");
- const dateButton = await page.locator('[data-testid="date-range"]');
+ const addOOOButton = page.getByTestId("add_entry_ooo");
+ const dateButton = page.locator('[data-testid="date-range"]');
+ const reasonListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200
+ );
+ await addOOOButton.click();
+ await reasonListRespPromise;
//As owner,OOO is created on Next month 1st - 3rd, forwarding to 'member-1'
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
await dateButton.click();
await selectDateAndCreateOOO(page, "1", "3", "member-1");
await expect(
@@ -366,8 +425,10 @@ test.describe("Out of office", () => {
//As member1, OOO is created on Next month 4th - 5th, forwarding to 'owner'
await member1User?.apiLogin();
await page.goto("/settings/my-account/out-of-office");
- await page.waitForLoadState();
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
+ await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
+ await addOOOButton.click();
+ await reasonListRespPromise;
await dateButton.click();
await selectDateAndCreateOOO(page, "4", "5", "owner");
await expect(page.locator(`data-testid=table-redirect-${owner.username ?? "n-a"}`).nth(0)).toBeVisible();
@@ -388,14 +449,22 @@ test.describe("Out of office", () => {
await owner.apiLogin();
+ const entriesListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200
+ );
await page.goto("/settings/my-account/out-of-office");
- await page.waitForLoadState();
+ await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
- const addOOOButton = await page.getByTestId("add_entry_ooo");
- const dateButton = await page.locator('[data-testid="date-range"]');
+ const addOOOButton = page.getByTestId("add_entry_ooo");
+ const dateButton = page.locator('[data-testid="date-range"]');
+ const reasonListRespPromise = page.waitForResponse(
+ (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200
+ );
+ await addOOOButton.click();
+ await reasonListRespPromise;
//As owner,OOO is created on Next month 1st - 3rd, forwarding to 'member-1'
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
await dateButton.click();
await selectDateAndCreateOOO(page, "1", "3", "member-1");
await expect(
@@ -405,8 +474,10 @@ test.describe("Out of office", () => {
//As member1, expect error while OOO is created on Next month 2nd - 5th, forwarding to 'owner'
await member1User?.apiLogin();
await page.goto("/settings/my-account/out-of-office");
- await page.waitForLoadState();
- await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1");
+ await page.waitForLoadState("domcontentloaded");
+ await entriesListRespPromise;
+ await addOOOButton.click();
+ await reasonListRespPromise;
await dateButton.click();
await selectDateAndCreateOOO(page, "2", "5", "owner", 400);
await expect(page.locator(`text=${t("booking_redirect_infinite_not_allowed")}`)).toBeTruthy();
diff --git a/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx b/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx
index f455de2b9a10b3..c4fd176dc38dff 100644
--- a/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx
+++ b/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx
@@ -10,7 +10,7 @@ export default function HowToUse() {
-
+
How to route a Typeform with Cal.com Routing
diff --git a/packages/app-store/typeform/static/icon.svg b/packages/app-store/typeform/static/icon.svg
new file mode 100644
index 00000000000000..5ff1b4ae2060cf
--- /dev/null
+++ b/packages/app-store/typeform/static/icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/features/ee/payments/components/PaymentPage.tsx b/packages/features/ee/payments/components/PaymentPage.tsx
index 626acdafcd3210..136d7fdf585645 100644
--- a/packages/features/ee/payments/components/PaymentPage.tsx
+++ b/packages/features/ee/payments/components/PaymentPage.tsx
@@ -113,7 +113,7 @@ const PaymentPage: FC
= (props) => {
{eventName}
{t("when")}
- {date.format("dddd, DD MMMM YYYY")}
+ {date.locale(i18n.language).format("dddd, DD MMMM YYYY")}
{date.format(is24h ? "H:mm" : "h:mma")} - {props.eventType.length} mins{" "}
({timezone})
diff --git a/packages/features/shell/navigation/Navigation.tsx b/packages/features/shell/navigation/Navigation.tsx
index 7d71117f19fff0..101734fbc5db0f 100644
--- a/packages/features/shell/navigation/Navigation.tsx
+++ b/packages/features/shell/navigation/Navigation.tsx
@@ -219,7 +219,7 @@ const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigati
<>