mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 18:18:48 -06:00
chore: Refactored the intercom next public env variable and added test files (#4960)
This commit is contained in:
@@ -207,7 +207,7 @@ UNKEY_ROOT_KEY=
|
||||
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
|
||||
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
||||
|
||||
# NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||
# INTERCOM_APP_ID=
|
||||
# INTERCOM_SECRET_KEY=
|
||||
|
||||
# Enable Prometheus metrics
|
||||
|
||||
@@ -12,7 +12,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import React from "react";
|
||||
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import React from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
|
||||
export const SettingsCard = ({
|
||||
|
||||
92
apps/web/app/(app)/layout.test.tsx
Normal file
92
apps/web/app/(app)/layout.test.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import AppLayout from "./layout";
|
||||
|
||||
vi.mock("next-auth", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
INTERCOM_SECRET_KEY: "test-secret-key",
|
||||
IS_INTERCOM_CONFIGURED: true,
|
||||
INTERCOM_APP_ID: "test-app-id",
|
||||
ENCRYPTION_KEY: "test-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "test-enterprise-license-key",
|
||||
GITHUB_ID: "test-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||
OIDC_ISSUER: "test-oidc-issuer",
|
||||
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "test-webapp-url",
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
|
||||
FormbricksClient: () => <div data-testid="formbricks-client" />,
|
||||
}));
|
||||
vi.mock("@/app/intercom/IntercomClientWrapper", () => ({
|
||||
IntercomClientWrapper: () => <div data-testid="mock-intercom-wrapper" />,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({
|
||||
NoMobileOverlay: () => <div data-testid="no-mobile-overlay" />,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/post-hog-client", () => ({
|
||||
PHProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="ph-provider">{children}</div>
|
||||
),
|
||||
PostHogPageview: () => <div data-testid="ph-pageview" />,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/toaster-client", () => ({
|
||||
ToasterClient: () => <div data-testid="toaster-client" />,
|
||||
}));
|
||||
|
||||
describe("(app) AppLayout", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders child content and all sub-components when user exists", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
|
||||
|
||||
// Because AppLayout is async, call it like a function
|
||||
const element = await AppLayout({
|
||||
children: <div data-testid="child-content">Hello from children</div>,
|
||||
});
|
||||
|
||||
render(element);
|
||||
|
||||
expect(screen.getByTestId("no-mobile-overlay")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("ph-pageview")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("ph-provider")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-intercom-wrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("toaster-client")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children");
|
||||
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("skips FormbricksClient if no user is present", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||
|
||||
const element = await AppLayout({
|
||||
children: <div data-testid="child-content">Hello from children</div>,
|
||||
});
|
||||
render(element);
|
||||
|
||||
expect(screen.queryByTestId("formbricks-client")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
|
||||
import { IntercomClient } from "@/app/IntercomClient";
|
||||
import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
|
||||
import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client";
|
||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { Suspense } from "react";
|
||||
import { INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
|
||||
const AppLayout = async ({ children }) => {
|
||||
@@ -22,11 +21,7 @@ const AppLayout = async ({ children }) => {
|
||||
<PHProvider>
|
||||
<>
|
||||
{user ? <FormbricksClient userId={user.id} email={user.email} /> : null}
|
||||
<IntercomClient
|
||||
isIntercomConfigured={IS_INTERCOM_CONFIGURED}
|
||||
intercomSecretKey={INTERCOM_SECRET_KEY}
|
||||
user={user}
|
||||
/>
|
||||
<IntercomClientWrapper user={user} />
|
||||
<ToasterClient />
|
||||
{children}
|
||||
</>
|
||||
|
||||
34
apps/web/app/(auth)/layout.test.tsx
Normal file
34
apps/web/app/(auth)/layout.test.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import AppLayout from "../(auth)/layout";
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
IS_INTERCOM_CONFIGURED: true,
|
||||
INTERCOM_SECRET_KEY: "mock-intercom-secret-key",
|
||||
INTERCOM_APP_ID: "mock-intercom-app-id",
|
||||
}));
|
||||
|
||||
vi.mock("@/app/intercom/IntercomClientWrapper", () => ({
|
||||
IntercomClientWrapper: () => <div data-testid="mock-intercom-wrapper" />,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/no-mobile-overlay", () => ({
|
||||
NoMobileOverlay: () => <div data-testid="mock-no-mobile-overlay" />,
|
||||
}));
|
||||
|
||||
describe("(auth) AppLayout", () => {
|
||||
it("renders the NoMobileOverlay and IntercomClient, plus children", async () => {
|
||||
const appLayoutElement = await AppLayout({
|
||||
children: <div data-testid="child-content">Hello from children!</div>,
|
||||
});
|
||||
|
||||
const childContentText = "Hello from children!";
|
||||
|
||||
render(appLayoutElement);
|
||||
|
||||
expect(screen.getByTestId("mock-no-mobile-overlay")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-intercom-wrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("child-content")).toHaveTextContent(childContentText);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
import { IntercomClient } from "@/app/IntercomClient";
|
||||
import { IntercomClientWrapper } from "@/app/intercom/IntercomClientWrapper";
|
||||
import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay";
|
||||
import { INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants";
|
||||
|
||||
const AppLayout = async ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<NoMobileOverlay />
|
||||
<IntercomClient isIntercomConfigured={IS_INTERCOM_CONFIGURED} intercomSecretKey={INTERCOM_SECRET_KEY} />
|
||||
<IntercomClientWrapper />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
186
apps/web/app/intercom/IntercomClient.test.tsx
Normal file
186
apps/web/app/intercom/IntercomClient.test.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import Intercom from "@intercom/messenger-js-sdk";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { IntercomClient } from "./IntercomClient";
|
||||
|
||||
// Mock the Intercom package
|
||||
vi.mock("@intercom/messenger-js-sdk", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("IntercomClient", () => {
|
||||
let originalWindowIntercom: any;
|
||||
let mockWindowIntercom = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original window.Intercom so we can restore it later
|
||||
originalWindowIntercom = global.window?.Intercom;
|
||||
// Mock window.Intercom so we can verify the shutdown call on unmount
|
||||
global.window.Intercom = mockWindowIntercom;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
// Restore the original window.Intercom
|
||||
global.window.Intercom = originalWindowIntercom;
|
||||
});
|
||||
|
||||
it("calls Intercom with user data when isIntercomConfigured is true and user is provided", () => {
|
||||
const testUser = {
|
||||
id: "test-id",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
createdAt: new Date("2020-01-01T00:00:00Z"),
|
||||
} as TUser;
|
||||
|
||||
render(
|
||||
<IntercomClient
|
||||
isIntercomConfigured={true}
|
||||
intercomUserHash="my-user-hash"
|
||||
intercomAppId="my-app-id"
|
||||
user={testUser}
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify Intercom was called with the expected params
|
||||
expect(Intercom).toHaveBeenCalledTimes(1);
|
||||
expect(Intercom).toHaveBeenCalledWith({
|
||||
app_id: "my-app-id",
|
||||
user_id: "test-id",
|
||||
user_hash: "my-user-hash",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
created_at: 1577836800, // Epoch for 2020-01-01T00:00:00Z
|
||||
});
|
||||
});
|
||||
|
||||
it("calls Intercom with user data without createdAt", () => {
|
||||
const testUser = {
|
||||
id: "test-id",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
} as TUser;
|
||||
|
||||
render(
|
||||
<IntercomClient
|
||||
isIntercomConfigured={true}
|
||||
intercomUserHash="my-user-hash"
|
||||
intercomAppId="my-app-id"
|
||||
user={testUser}
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify Intercom was called with the expected params
|
||||
expect(Intercom).toHaveBeenCalledTimes(1);
|
||||
expect(Intercom).toHaveBeenCalledWith({
|
||||
app_id: "my-app-id",
|
||||
user_id: "test-id",
|
||||
user_hash: "my-user-hash",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
created_at: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("calls Intercom with minimal params if user is not provided", () => {
|
||||
render(
|
||||
<IntercomClient isIntercomConfigured={true} intercomAppId="my-app-id" intercomUserHash="my-user-hash" />
|
||||
);
|
||||
|
||||
expect(Intercom).toHaveBeenCalledTimes(1);
|
||||
expect(Intercom).toHaveBeenCalledWith({
|
||||
app_id: "my-app-id",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call Intercom if isIntercomConfigured is false", () => {
|
||||
render(
|
||||
<IntercomClient
|
||||
isIntercomConfigured={false}
|
||||
intercomAppId="my-app-id"
|
||||
user={{ id: "whatever" } as TUser}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(Intercom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shuts down Intercom on unmount", () => {
|
||||
const { unmount } = render(
|
||||
<IntercomClient isIntercomConfigured={true} intercomAppId="my-app-id" intercomUserHash="my-user-hash" />
|
||||
);
|
||||
|
||||
// Reset call count; we only care about the shutdown after unmount
|
||||
mockWindowIntercom.mockClear();
|
||||
|
||||
unmount();
|
||||
|
||||
// Intercom should be shut down on unmount
|
||||
expect(mockWindowIntercom).toHaveBeenCalledWith("shutdown");
|
||||
});
|
||||
|
||||
it("logs an error if Intercom initialization fails", () => {
|
||||
// Spy on console.error
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
// Force Intercom to throw an error on invocation
|
||||
vi.mocked(Intercom).mockImplementationOnce(() => {
|
||||
throw new Error("Intercom test error");
|
||||
});
|
||||
|
||||
// Render the component with isIntercomConfigured=true so it tries to initialize
|
||||
render(
|
||||
<IntercomClient isIntercomConfigured={true} intercomAppId="my-app-id" intercomUserHash="my-user-hash" />
|
||||
);
|
||||
|
||||
// Verify that console.error was called with the correct message
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error));
|
||||
|
||||
// Clean up the spy
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("logs an error if isIntercomConfigured is true but no intercomAppId is provided", () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
render(
|
||||
<IntercomClient
|
||||
isIntercomConfigured={true}
|
||||
// missing intercomAppId
|
||||
intercomUserHash="my-user-hash"
|
||||
/>
|
||||
);
|
||||
|
||||
// We expect a caught error: "Intercom app ID is required"
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error));
|
||||
const [, caughtError] = consoleErrorSpy.mock.calls[0];
|
||||
expect((caughtError as Error).message).toBe("Intercom app ID is required");
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("logs an error if isIntercomConfigured is true but no intercomUserHash is provided", () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const testUser = {
|
||||
id: "test-id",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
} as TUser;
|
||||
|
||||
render(
|
||||
<IntercomClient
|
||||
isIntercomConfigured={true}
|
||||
intercomAppId="some-app-id"
|
||||
user={testUser}
|
||||
// missing intercomUserHash
|
||||
/>
|
||||
);
|
||||
|
||||
// We expect a caught error: "Intercom user hash is required"
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to initialize Intercom:", expect.any(Error));
|
||||
const [, caughtError] = consoleErrorSpy.mock.calls[0];
|
||||
expect((caughtError as Error).message).toBe("Intercom user hash is required");
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,30 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import Intercom from "@intercom/messenger-js-sdk";
|
||||
import { createHmac } from "crypto";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { env } from "@formbricks/lib/env";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
const intercomAppId = env.NEXT_PUBLIC_INTERCOM_APP_ID;
|
||||
|
||||
interface IntercomClientProps {
|
||||
isIntercomConfigured: boolean;
|
||||
intercomSecretKey?: string;
|
||||
intercomUserHash?: string;
|
||||
user?: TUser | null;
|
||||
intercomAppId?: string;
|
||||
}
|
||||
|
||||
export const IntercomClient = ({ user, intercomSecretKey, isIntercomConfigured }: IntercomClientProps) => {
|
||||
export const IntercomClient = ({
|
||||
user,
|
||||
intercomUserHash,
|
||||
isIntercomConfigured,
|
||||
intercomAppId,
|
||||
}: IntercomClientProps) => {
|
||||
const initializeIntercom = useCallback(() => {
|
||||
let initParams = {};
|
||||
|
||||
if (user) {
|
||||
if (user && intercomUserHash) {
|
||||
const { id, name, email, createdAt } = user;
|
||||
const hash = createHmac("sha256", intercomSecretKey!).update(user?.id).digest("hex");
|
||||
|
||||
initParams = {
|
||||
user_id: id,
|
||||
user_hash: hash,
|
||||
user_hash: intercomUserHash,
|
||||
name,
|
||||
email,
|
||||
created_at: createdAt ? Math.floor(createdAt.getTime() / 1000) : undefined,
|
||||
@@ -35,11 +36,21 @@ export const IntercomClient = ({ user, intercomSecretKey, isIntercomConfigured }
|
||||
app_id: intercomAppId!,
|
||||
...initParams,
|
||||
});
|
||||
}, [user, intercomSecretKey]);
|
||||
}, [user, intercomUserHash, intercomAppId]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (isIntercomConfigured) initializeIntercom();
|
||||
if (isIntercomConfigured) {
|
||||
if (!intercomAppId) {
|
||||
throw new Error("Intercom app ID is required");
|
||||
}
|
||||
|
||||
if (user && !intercomUserHash) {
|
||||
throw new Error("Intercom user hash is required");
|
||||
}
|
||||
|
||||
initializeIntercom();
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Shutdown Intercom when component unmounts
|
||||
@@ -50,7 +61,7 @@ export const IntercomClient = ({ user, intercomSecretKey, isIntercomConfigured }
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize Intercom:", error);
|
||||
}
|
||||
}, [isIntercomConfigured, initializeIntercom]);
|
||||
}, [isIntercomConfigured, initializeIntercom, intercomAppId, intercomUserHash, user]);
|
||||
|
||||
return null;
|
||||
};
|
||||
64
apps/web/app/intercom/IntercomClientWrapper.test.tsx
Normal file
64
apps/web/app/intercom/IntercomClientWrapper.test.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { IntercomClientWrapper } from "./IntercomClientWrapper";
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
IS_INTERCOM_CONFIGURED: true,
|
||||
INTERCOM_APP_ID: "mock-intercom-app-id",
|
||||
INTERCOM_SECRET_KEY: "mock-intercom-secret-key",
|
||||
}));
|
||||
|
||||
// Mock the crypto createHmac function to return a fake hash.
|
||||
// Vite global setup doesn't work here due to Intercom probably using crypto themselves.
|
||||
vi.mock("crypto", () => ({
|
||||
default: {
|
||||
createHmac: vi.fn(() => ({
|
||||
update: vi.fn().mockReturnThis(),
|
||||
digest: vi.fn().mockReturnValue("fake-hash"),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./IntercomClient", () => ({
|
||||
IntercomClient: (props: any) => (
|
||||
<div data-testid="mock-intercom-client" data-props={JSON.stringify(props)} />
|
||||
),
|
||||
}));
|
||||
|
||||
describe("IntercomClientWrapper", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders IntercomClient with computed user hash when user is provided", () => {
|
||||
const testUser = { id: "user-123", name: "Test User", email: "test@example.com" } as TUser;
|
||||
|
||||
render(<IntercomClientWrapper user={testUser} />);
|
||||
|
||||
const intercomClientEl = screen.getByTestId("mock-intercom-client");
|
||||
expect(intercomClientEl).toBeInTheDocument();
|
||||
|
||||
const props = JSON.parse(intercomClientEl.getAttribute("data-props") ?? "{}");
|
||||
|
||||
// Check that the computed hash equals "fake-hash" (as per our crypto mock)
|
||||
expect(props.intercomUserHash).toBe("fake-hash");
|
||||
expect(props.intercomAppId).toBe("mock-intercom-app-id");
|
||||
expect(props.isIntercomConfigured).toBe(true);
|
||||
expect(props.user).toEqual(testUser);
|
||||
});
|
||||
|
||||
it("renders IntercomClient without computing a hash when no user is provided", () => {
|
||||
render(<IntercomClientWrapper user={null} />);
|
||||
|
||||
const intercomClientEl = screen.getByTestId("mock-intercom-client");
|
||||
expect(intercomClientEl).toBeInTheDocument();
|
||||
|
||||
const props = JSON.parse(intercomClientEl.getAttribute("data-props") ?? "{}");
|
||||
|
||||
expect(props.intercomUserHash).toBeUndefined();
|
||||
expect(props.intercomAppId).toBe("mock-intercom-app-id");
|
||||
expect(props.isIntercomConfigured).toBe(true);
|
||||
expect(props.user).toBeNull();
|
||||
});
|
||||
});
|
||||
26
apps/web/app/intercom/IntercomClientWrapper.tsx
Normal file
26
apps/web/app/intercom/IntercomClientWrapper.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createHmac } from "crypto";
|
||||
import { INTERCOM_APP_ID, INTERCOM_SECRET_KEY, IS_INTERCOM_CONFIGURED } from "@formbricks/lib/constants";
|
||||
import type { TUser } from "@formbricks/types/user";
|
||||
import { IntercomClient } from "./IntercomClient";
|
||||
|
||||
interface IntercomClientWrapperProps {
|
||||
user?: TUser | null;
|
||||
}
|
||||
|
||||
export const IntercomClientWrapper = ({ user }: IntercomClientWrapperProps) => {
|
||||
let intercomUserHash: string | undefined;
|
||||
if (user) {
|
||||
const secretKey = INTERCOM_SECRET_KEY;
|
||||
if (secretKey) {
|
||||
intercomUserHash = createHmac("sha256", secretKey).update(user.id).digest("hex");
|
||||
}
|
||||
}
|
||||
return (
|
||||
<IntercomClient
|
||||
isIntercomConfigured={IS_INTERCOM_CONFIGURED}
|
||||
user={user}
|
||||
intercomAppId={INTERCOM_APP_ID}
|
||||
intercomUserHash={intercomUserHash}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { hashApiKey } from "../utils";
|
||||
describe("hashApiKey", () => {
|
||||
test("generate the correct sha256 hash for a given input", () => {
|
||||
const input = "test";
|
||||
const expectedHash = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
|
||||
const expectedHash = "fake-hash"; // mocked on the vitestSetup.ts file;
|
||||
const result = hashApiKey(input);
|
||||
expect(result).toEqual(expectedHash);
|
||||
});
|
||||
@@ -12,19 +12,6 @@ describe("hashApiKey", () => {
|
||||
test("return a string with length 64", () => {
|
||||
const input = "another-api-key";
|
||||
const result = hashApiKey(input);
|
||||
expect(result).toHaveLength(64);
|
||||
});
|
||||
|
||||
test("produce the same hash for identical inputs", () => {
|
||||
const input = "consistentKey";
|
||||
const firstHash = hashApiKey(input);
|
||||
const secondHash = hashApiKey(input);
|
||||
expect(firstHash).toEqual(secondHash);
|
||||
});
|
||||
|
||||
test("generate different hashes for different inputs", () => {
|
||||
const hash1 = hashApiKey("key1");
|
||||
const hash2 = hashApiKey("key2");
|
||||
expect(hash1).not.toEqual(hash2);
|
||||
expect(result).toHaveLength(9); // mocked on the vitestSetup.ts file;;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { debounce } from "lodash";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { VisibilityState, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TContactTableData } from "../types/contact";
|
||||
import { generateContactTableColumns } from "./contact-table-column";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { TContactCSVUploadResponse } from "@/modules/ee/contacts/types/contact";
|
||||
import React from "react";
|
||||
|
||||
interface CsvTableProps {
|
||||
data: TContactCSVUploadResponse;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PasswordInput } from "@/modules/ui/components/password-input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { EnterCode } from "@/modules/ee/two-factor-auth/components/enter-code";
|
||||
import { ScanQRCode } from "@/modules/ee/two-factor-auth/components/scan-qr-code";
|
||||
import { Modal } from "@/modules/ui/components/modal";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
export type EnableTwoFactorModalStep = "confirmPassword" | "scanQRCode" | "enterCode" | "backupCodes";
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { FormField, FormItem } from "@/modules/ui/components/form";
|
||||
import { FormControl } from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import React from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
interface TwoFactorBackupProps {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { FormControl, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
import { OTPInput } from "@/modules/ui/components/otp-input";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import React from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
interface TwoFactorProps {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { uploadFile } from "@/modules/ui/components/file-input/lib/utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { TFnType } from "@tolgee/react";
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { EmailTemplate } from "./email-template";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { DefaultParamType, TFnType, TranslationKey } from "@tolgee/react/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { FollowUpEmail } from "./follow-up";
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
@@ -29,6 +29,10 @@ describe("FollowUpEmail", () => {
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders the default logo if no custom logo is provided", async () => {
|
||||
const followUpEmailElement = await FollowUpEmail({
|
||||
...defaultProps,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { SendHorizonalIcon, ShareIcon, TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TMember } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
|
||||
import { truncate } from "@formbricks/lib/utils/strings";
|
||||
|
||||
@@ -14,7 +14,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DeleteAccountModal } from "@/modules/account/components/DeleteAccountMo
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
interface RemovedFromOrganizationProps {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { LanguageIndicator } from "@/modules/ee/multi-language-surveys/components/language-indicator";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import React, { ReactNode, useMemo } from "react";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { getEnabledLanguages } from "@formbricks/lib/i18n/utils";
|
||||
import { headlineToRecall, recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TI18nString, TSurvey, TSurveyRecallItem } from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { KeyIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
export type ModalButton = {
|
||||
text: string;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "react",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@prisma/client/*": ["@formbricks/database/client/*"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// vitest.config.ts
|
||||
import { loadEnv } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
@@ -26,6 +27,9 @@ export default defineConfig({
|
||||
"modules/email/emails/survey/follow-up.tsx",
|
||||
"app/(app)/environments/**/settings/(organization)/general/page.tsx",
|
||||
"modules/ee/sso/lib/**/*.ts",
|
||||
"app/(auth)/layout.tsx",
|
||||
"app/(app)/layout.tsx",
|
||||
"app/intercom/*.tsx",
|
||||
],
|
||||
exclude: [
|
||||
"**/.next/**",
|
||||
@@ -39,5 +43,5 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [tsconfigPaths()],
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
});
|
||||
|
||||
@@ -270,8 +270,9 @@ export const IS_AI_CONFIGURED = Boolean(
|
||||
);
|
||||
|
||||
export const INTERCOM_SECRET_KEY = env.INTERCOM_SECRET_KEY;
|
||||
export const INTERCOM_APP_ID = env.INTERCOM_APP_ID;
|
||||
|
||||
export const IS_INTERCOM_CONFIGURED = Boolean(env.NEXT_PUBLIC_INTERCOM_APP_ID && INTERCOM_SECRET_KEY);
|
||||
export const IS_INTERCOM_CONFIGURED = Boolean(env.INTERCOM_APP_ID && INTERCOM_SECRET_KEY);
|
||||
|
||||
export const TURNSTILE_SECRET_KEY = env.TURNSTILE_SECRET_KEY;
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export const env = createEnv({
|
||||
IMPRINT_ADDRESS: z.string().optional(),
|
||||
INVITE_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
INTERCOM_SECRET_KEY: z.string().optional(),
|
||||
INTERCOM_APP_ID: z.string().optional(),
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
MAIL_FROM: z.string().email().optional(),
|
||||
MAIL_FROM_NAME: z.string().optional(),
|
||||
@@ -125,7 +126,6 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_POSTHOG_API_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST: z.string().optional(),
|
||||
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
|
||||
NEXT_PUBLIC_INTERCOM_APP_ID: z.string().optional(),
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string().optional(),
|
||||
},
|
||||
/*
|
||||
@@ -187,7 +187,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
|
||||
OPENTELEMETRY_LISTENER_URL: process.env.OPENTELEMETRY_LISTENER_URL,
|
||||
NEXT_PUBLIC_INTERCOM_APP_ID: process.env.NEXT_PUBLIC_INTERCOM_APP_ID,
|
||||
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID,
|
||||
NOTION_OAUTH_CLIENT_ID: process.env.NOTION_OAUTH_CLIENT_ID,
|
||||
NOTION_OAUTH_CLIENT_SECRET: process.env.NOTION_OAUTH_CLIENT_SECRET,
|
||||
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,
|
||||
|
||||
@@ -3,6 +3,8 @@ import "@testing-library/jest-dom/vitest";
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { ValidationError } from "@formbricks/types/errors";
|
||||
|
||||
// mock next cache
|
||||
|
||||
vi.mock("next/cache", () => ({
|
||||
__esModule: true,
|
||||
unstable_cache: (fn: (params: unknown[]) => {}) => {
|
||||
@@ -23,6 +25,8 @@ vi.mock("react", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// mock tolgee useTranslate on components
|
||||
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => {
|
||||
return {
|
||||
@@ -31,6 +35,8 @@ vi.mock("@tolgee/react", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// mock next/router navigation
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
@@ -47,6 +53,8 @@ vi.mock("server-only", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
// mock prisma client
|
||||
|
||||
vi.mock("@prisma/client", async () => {
|
||||
const actual = await vi.importActual<typeof import("@prisma/client")>("@prisma/client");
|
||||
|
||||
@@ -67,6 +75,8 @@ vi.mock("@prisma/client", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// mock URL object
|
||||
|
||||
if (typeof URL.revokeObjectURL !== "function") {
|
||||
URL.revokeObjectURL = () => {};
|
||||
}
|
||||
@@ -75,6 +85,28 @@ if (typeof URL.createObjectURL !== "function") {
|
||||
URL.createObjectURL = () => "blob://fake-url";
|
||||
}
|
||||
|
||||
// mock crypto function used in the license check utils, every component that checks the license will use this mock
|
||||
// also used in a lot of other places too
|
||||
vi.mock("crypto", async () => {
|
||||
const actual = await vi.importActual<typeof import("crypto")>("crypto");
|
||||
|
||||
return {
|
||||
...actual,
|
||||
|
||||
createHash: () => ({
|
||||
update: vi.fn().mockReturnThis(),
|
||||
digest: vi.fn().mockReturnValue("fake-hash"),
|
||||
}),
|
||||
default: {
|
||||
...actual,
|
||||
createHash: () => ({
|
||||
update: vi.fn().mockReturnThis(),
|
||||
digest: vi.fn().mockReturnValue("fake-hash"),
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.resetAllMocks();
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
"IMPRINT_ADDRESS",
|
||||
"INVITE_DISABLED",
|
||||
"IS_FORMBRICKS_CLOUD",
|
||||
"INTERCOM_APP_ID",
|
||||
"INTERCOM_SECRET_KEY",
|
||||
"MAIL_FROM",
|
||||
"MAIL_FROM_NAME",
|
||||
@@ -135,7 +136,6 @@
|
||||
"NEXT_PUBLIC_FORMBRICKS_URL",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"NEXT_PUBLIC_POSTHOG_API_HOST",
|
||||
"NEXT_PUBLIC_INTERCOM_APP_ID",
|
||||
"NEXT_PUBLIC_POSTHOG_API_KEY",
|
||||
"NEXT_PUBLIC_FORMBRICKS_COM_API_HOST",
|
||||
"NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID",
|
||||
|
||||
Reference in New Issue
Block a user