fix: App Crash inside Embed when Cross Domain cookies are disabled in browser(Firefox) (#22679)

* Fix: embed crash when third party cookies are disabled

* :Fix: embed crash when third party cookies are disabled

* fix missing import
This commit is contained in:
Hariom Balhara
2025-07-25 07:44:54 +04:00
committed by GitHub
parent 1d28215c5e
commit cc6beedfb0
5 changed files with 303 additions and 32 deletions
@@ -0,0 +1,157 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { applyTheme } from "./_applyThemeForDocument";
describe("applyTheme", () => {
let reconstructedFn: () => void;
let mockLocalStorage: Storage;
let mockRequestAnimationFrame: typeof window.requestAnimationFrame;
let mockDocument: Document;
let addedClasses: string[] = [];
beforeEach(() => {
// Create the function from stringified version
const stringifiedFunction = applyTheme.toString();
reconstructedFn = new Function(`return ${stringifiedFunction}`)();
// Mock localStorage
mockLocalStorage = {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
key: vi.fn(() => null),
length: 0,
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.localStorage = mockLocalStorage;
// Mock requestAnimationFrame
mockRequestAnimationFrame = vi.fn((cb) => {
cb(0);
return 0;
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.window = { location: { pathname: "/" } };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.requestAnimationFrame = mockRequestAnimationFrame;
// Mock document
addedClasses = [];
mockDocument = {
body: {
classList: {
add: vi.fn((className: string) => {
addedClasses.push(className);
}),
},
},
} as unknown as Document;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.document = mockDocument;
// Mock console.error
global.console.error = vi.fn();
});
afterEach(() => {
vi.clearAllMocks();
});
it("should be reconstructable from toString() and executable", () => {
expect(() => reconstructedFn()).not.toThrow();
});
it("should not add any theme when no app-theme is found", () => {
reconstructedFn();
expect(mockRequestAnimationFrame).not.toHaveBeenCalled();
expect(addedClasses).toHaveLength(0);
});
it("should handle localStorage errors gracefully", () => {
// Make localStorage throw an error
mockLocalStorage.getItem = vi.fn(() => {
throw new Error("localStorage not available");
});
mockLocalStorage.key = vi.fn(() => {
throw new Error("localStorage not available");
});
Object.defineProperty(mockLocalStorage, "length", {
get: () => {
throw new Error("localStorage not available");
},
});
expect(() => reconstructedFn()).not.toThrow();
expect(addedClasses).toHaveLength(0);
});
it("should apply booking theme when app-theme exists and on booking page", () => {
const username = "testuser";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.window.location.pathname = `/${username}/meeting`;
mockLocalStorage.key = vi.fn((index: number) => {
if (index === 0) return `booking-theme:${username}`;
return null;
});
mockLocalStorage.getItem = vi.fn((key: string) => {
if (key === "app-theme") return "light";
if (key === `booking-theme:${username}`) return "dark";
return null;
});
Object.defineProperty(mockLocalStorage, "length", {
get: () => 1,
configurable: true,
});
reconstructedFn();
expect(addedClasses).toContain("dark");
});
it("should apply app theme when not on booking page", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
global.window.location.pathname = "/some-other-page";
mockLocalStorage.key = vi.fn((index: number) => {
if (index === 0) return "booking-theme:testuser";
return null;
});
mockLocalStorage.getItem = vi.fn((key: string) => {
if (key === "app-theme") return "light";
if (key === "booking-theme:testuser") return "dark";
return null;
});
Object.defineProperty(mockLocalStorage, "length", {
get: () => 1,
configurable: true,
});
reconstructedFn();
expect(addedClasses).toContain("light");
});
it("should apply app theme when app-theme exists but no booking theme", () => {
mockLocalStorage.getItem = vi.fn((key: string) => {
if (key === "app-theme") return "dark";
return null;
});
Object.defineProperty(mockLocalStorage, "length", {
get: () => 0,
configurable: true,
});
reconstructedFn();
expect(addedClasses).toContain("dark");
});
});
+59
View File
@@ -0,0 +1,59 @@
// Theme application function - will be stringified and injected. So, it must not use anything from the closure
export const applyTheme = function () {
try {
// This utility is a replica of @calcom/lib/webstorage.ts but we can reuse it because applyTheme is stringified and injected, so we can't have deps here
const safeLocalStorage = {
getItem: function (key: string) {
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
return localStorage.getItem(key);
} catch (e) {
return null;
}
},
key: function (index: number) {
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
return localStorage.key(index);
} catch (e) {
return null;
}
},
getLength: function () {
try {
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
return localStorage.length;
} catch (e) {
return 0;
}
},
};
const appTheme = safeLocalStorage.getItem("app-theme");
if (!appTheme) return;
let bookingTheme: string | null = null;
let username: string | undefined;
for (let i = 0; i < safeLocalStorage.getLength(); i++) {
const key = safeLocalStorage.key(i);
if (key && key.startsWith("booking-theme:")) {
bookingTheme = safeLocalStorage.getItem(key);
username = key.split("booking-theme:")[1];
break;
}
}
const onReady = () => {
const isBookingPage = username && window.location.pathname.slice(1).startsWith(username);
if (document.body) {
document.body.classList.add(isBookingPage ? bookingTheme || appTheme : appTheme);
} else {
requestAnimationFrame(onReady);
}
};
requestAnimationFrame(onReady);
} catch (e) {
console.error("Error applying theme:", e);
}
};
+3 -30
View File
@@ -9,6 +9,8 @@ import { IS_PRODUCTION } from "@calcom/lib/constants";
import { csp } from "@lib/csp";
import { applyTheme } from "./_applyThemeForDocument";
type Props = Record<string, unknown> & DocumentProps & { newLocale: string };
function setHeader(ctx: NextPageContext, name: string, value: string) {
try {
@@ -70,36 +72,7 @@ class MyDocument extends Document<Props> {
dangerouslySetInnerHTML={{
__html: `
window.calNewLocale = "${newLocale}";
(function applyTheme() {
try {
const appTheme = localStorage.getItem('app-theme');
if (!appTheme) return;
let bookingTheme, username;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('booking-theme:')) {
bookingTheme = localStorage.getItem(key);
username = key.split("booking-theme:")[1];
break;
}
}
const onReady = () => {
const isBookingPage = username && window.location.pathname.slice(1).startsWith(username);
if (document.body) {
document.body.classList.add(isBookingPage ? bookingTheme : appTheme);
} else {
requestAnimationFrame(onReady);
}
};
requestAnimationFrame(onReady);
} catch (e) {
console.error('Error applying theme:', e);
}
})();
(${applyTheme.toString()})();
`,
}}
/>
@@ -3,10 +3,27 @@ import { ESLintUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator((name) => `https://developer.cal.com/eslint/rule/${name}`);
const rule = createRule({
create(context) {
// Track imported names from @calcom/lib/webstorage
const safeImportedNames = new Set<string>();
return {
ImportDeclaration(node) {
// Check if this is an import from the safe webstorage module
if (node.source.value === "@calcom/lib/webstorage") {
node.specifiers.forEach((specifier) => {
if (specifier.type === "ImportSpecifier") {
// Track the local name used for the import
safeImportedNames.add(specifier.local.name);
}
});
}
},
CallExpression(node) {
const webStorages = ["localStorage", "sessionStorage"];
const callee = node.callee;
// Check for window.localStorage or window.sessionStorage
if (
// Can't figure out how to fix this TS issue
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -23,6 +40,71 @@ const rule = createRule({
messageId: "possible-issue-with-embed",
});
}
// Check for direct localStorage or sessionStorage usage
if (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
callee.type === "MemberExpression" &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
callee.object?.type === "Identifier" &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
webStorages.includes(callee.object?.name) &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
!safeImportedNames.has(callee.object?.name)
) {
return context.report({
node: node,
loc: node.loc,
messageId: "possible-issue-with-embed",
});
}
},
// Also check for property access like localStorage.length
MemberExpression(node) {
const webStorages = ["localStorage", "sessionStorage"];
// Check for direct property access on localStorage/sessionStorage
if (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
node.object?.type === "Identifier" &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
webStorages.includes(node.object?.name) &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
!safeImportedNames.has(node.object?.name)
) {
return context.report({
node: node,
loc: node.loc,
messageId: "possible-issue-with-embed",
});
}
// Check for window.localStorage/sessionStorage property access
if (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
node.object?.type === "MemberExpression" &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
node.object?.object?.name === "window" &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
webStorages.includes(node.object?.property?.name)
) {
return context.report({
node: node,
loc: node.loc,
messageId: "possible-issue-with-embed",
});
}
},
};
},
+2 -2
View File
@@ -2,6 +2,7 @@ import { useTheme as useNextTheme } from "next-themes";
import { useEffect } from "react";
import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
import { localStorage } from "@calcom/lib/webstorage";
/**
* It should be called once per route if you intend to use a theme different from `system` theme. `system` theme is automatically supported using <ThemeProvider />
@@ -12,8 +13,7 @@ import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
// eslint-disable-next-line @typescript-eslint/ban-types
export default function useTheme(themeToSet: "system" | (string & {}) | undefined | null, getOnly = false) {
if (typeof window !== "undefined") {
const themeFromLocalStorage =
typeof window !== "undefined" && window.localStorage ? localStorage.getItem("app-theme") : null;
const themeFromLocalStorage = localStorage.getItem("app-theme");
themeToSet = themeToSet ?? themeFromLocalStorage ?? "system";
}
const { resolvedTheme, setTheme, forcedTheme, theme: activeTheme } = useNextTheme();