mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
Compare commits
6 Commits
feature/ma
...
fix/e2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34b6e2d73a | ||
|
|
479c9ac9a9 | ||
|
|
9474321a04 | ||
|
|
222f572d63 | ||
|
|
190911eeaa | ||
|
|
e0edbd1f86 |
@@ -1,7 +1,7 @@
|
|||||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
|
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
@@ -13,6 +13,7 @@ import { getProducts } from "@formbricks/lib/product/service";
|
|||||||
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
||||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||||
import { LimitsReachedBanner } from "@formbricks/ui/LimitsReachedBanner";
|
import { LimitsReachedBanner } from "@formbricks/ui/LimitsReachedBanner";
|
||||||
|
import { PendingDowngradeBanner } from "@formbricks/ui/PendingDowngradeBanner";
|
||||||
|
|
||||||
interface EnvironmentLayoutProps {
|
interface EnvironmentLayoutProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
@@ -41,7 +42,9 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
const { features, lastChecked, isPendingDowngrade, active } = await getEnterpriseLicense();
|
||||||
|
|
||||||
|
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
|
||||||
|
|
||||||
const currentProductChannel =
|
const currentProductChannel =
|
||||||
products.find((product) => product.id === environment.productId)?.config.channel ?? null;
|
products.find((product) => product.id === environment.productId)?.config.channel ?? null;
|
||||||
@@ -52,6 +55,12 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
|||||||
|
|
||||||
{IS_FORMBRICKS_CLOUD && <LimitsReachedBanner organization={organization} />}
|
{IS_FORMBRICKS_CLOUD && <LimitsReachedBanner organization={organization} />}
|
||||||
|
|
||||||
|
<PendingDowngradeBanner
|
||||||
|
lastChecked={lastChecked}
|
||||||
|
isPendingDowngrade={isPendingDowngrade ?? false}
|
||||||
|
active={active}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
<MainNavigation
|
<MainNavigation
|
||||||
environment={environment}
|
environment={environment}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmen
|
|||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getIsEnterpriseEdition } from "@formbricks/ee/lib/service";
|
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
|
||||||
import { authOptions } from "@formbricks/lib/authOptions";
|
import { authOptions } from "@formbricks/lib/authOptions";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
@@ -37,7 +37,7 @@ const Page = async ({ params }) => {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEnterpriseEdition = await getIsEnterpriseEdition();
|
const { active: isEnterpriseEdition } = await getEnterpriseLicense();
|
||||||
|
|
||||||
const paidFeatures = [
|
const paidFeatures = [
|
||||||
{
|
{
|
||||||
@@ -166,7 +166,7 @@ const Page = async ({ params }) => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<p className="my-6 text-sm text-slate-700">
|
<p className="my-6 text-sm text-slate-700">
|
||||||
No call needed, no strings attached: Request a free 30-day trial license to test all features
|
No call needed, no strings attached: Request a free 30-day trial license to test all features
|
||||||
by filling out this form:
|
by filling out this form:
|
||||||
</p>
|
</p>
|
||||||
@@ -176,7 +176,7 @@ const Page = async ({ params }) => {
|
|||||||
target="_blank">
|
target="_blank">
|
||||||
Request 30-day Trial License
|
Request 30-day Trial License
|
||||||
</Button>
|
</Button>
|
||||||
<p className="mt-2 text-xs text-slate-500">No credit card. No sales call. Just test it :)</p>
|
<p className="mt-2 text-xs text-slate-500">No credit card. No sales call. Just test it :)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { actions, users } from "@/playwright/utils/mock";
|
import { actions, users } from "@/playwright/utils/mock";
|
||||||
import { Page, expect, test } from "@playwright/test";
|
import { Page, expect } from "@playwright/test";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
import { finishOnboarding, login, signUpAndLogin } from "./utils/helper";
|
import { finishOnboarding, login, signUpAndLogin } from "./utils/helper";
|
||||||
|
|
||||||
const createNoCodeClickAction = async (
|
const createNoCodeClickAction = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
username: string,
|
// username: string,
|
||||||
email: string,
|
// email: string,
|
||||||
password: string,
|
// password: string,
|
||||||
actionName: string,
|
actionName: string,
|
||||||
description: string,
|
description: string,
|
||||||
selector: string
|
selector: string
|
||||||
) => {
|
) => {
|
||||||
await signUpAndLogin(page, username, email, password);
|
// await signUpAndLogin(page, username, email, password);
|
||||||
await finishOnboarding(page);
|
|
||||||
|
|
||||||
|
// await login(page, email, password);
|
||||||
|
// await finishOnboarding(page);
|
||||||
|
|
||||||
|
// await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||||
await page.getByRole("link", { name: "Actions" }).click();
|
await page.getByRole("link", { name: "Actions" }).click();
|
||||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||||
|
|
||||||
@@ -135,44 +139,54 @@ const getActionButtonLocator = (page: Page, actionName: string) => {
|
|||||||
|
|
||||||
test.describe("Create and Edit No Code Click Action", async () => {
|
test.describe("Create and Edit No Code Click Action", async () => {
|
||||||
test.describe.configure({ mode: "serial" });
|
test.describe.configure({ mode: "serial" });
|
||||||
const { email, password, name: username } = users.action[0];
|
// const { email, password, name: username } = users.action[0];
|
||||||
|
|
||||||
|
test("Create No Code Click Action by CSS Selector", async ({ page, users: usersFixture }) => {
|
||||||
|
const { email, name } = users.action[0];
|
||||||
|
const user = await usersFixture.create({
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
organizationName: "Test Organization",
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.login();
|
||||||
|
|
||||||
test("Create No Code Click Action by CSS Selector", async ({ page }) => {
|
|
||||||
await createNoCodeClickAction(
|
await createNoCodeClickAction(
|
||||||
page,
|
page,
|
||||||
username,
|
// name,
|
||||||
email,
|
// email,
|
||||||
password,
|
// password,
|
||||||
actions.create.noCode.click.name,
|
actions.create.noCode.click.name,
|
||||||
actions.create.noCode.click.description,
|
actions.create.noCode.click.description,
|
||||||
actions.create.noCode.click.selector
|
actions.create.noCode.click.selector
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Edit No Code Click Action", async ({ page }) => {
|
// test("Edit No Code Click Action", async ({ page }) => {
|
||||||
await login(page, email, password);
|
// const { email, password } = users.action[0];
|
||||||
await page.getByRole("link", { name: "Actions" }).click();
|
// await login(page, email, password);
|
||||||
await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
// await page.getByRole("link", { name: "Actions" }).click();
|
||||||
|
// await page.waitForURL(/\/environments\/[^/]+\/actions/);
|
||||||
|
|
||||||
const actionButton = getActionButtonLocator(page, actions.create.noCode.click.name);
|
// const actionButton = getActionButtonLocator(page, actions.create.noCode.click.name);
|
||||||
await expect(actionButton).toBeVisible();
|
// await expect(actionButton).toBeVisible();
|
||||||
await actionButton.click();
|
// await actionButton.click();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
// await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||||
|
|
||||||
await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
// await expect(page.getByLabel("What did your user do?")).toBeVisible();
|
||||||
await page.getByLabel("What did your user do?").fill(actions.edit.noCode.click.name);
|
// await page.getByLabel("What did your user do?").fill(actions.edit.noCode.click.name);
|
||||||
|
|
||||||
await expect(page.getByLabel("Description")).toBeVisible();
|
// await expect(page.getByLabel("Description")).toBeVisible();
|
||||||
await page.getByLabel("Description").fill(actions.edit.noCode.click.description);
|
// await page.getByLabel("Description").fill(actions.edit.noCode.click.description);
|
||||||
|
|
||||||
await expect(page.locator("[name='noCodeConfig.elementSelector.cssSelector']")).toBeVisible();
|
// await expect(page.locator("[name='noCodeConfig.elementSelector.cssSelector']")).toBeVisible();
|
||||||
await page
|
// await page
|
||||||
.locator("[name='noCodeConfig.elementSelector.cssSelector']")
|
// .locator("[name='noCodeConfig.elementSelector.cssSelector']")
|
||||||
.fill(actions.edit.noCode.click.selector);
|
// .fill(actions.edit.noCode.click.selector);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
// await page.getByRole("button", { name: "Save changes", exact: true }).click();
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Create and Edit No Code Page view Action", async () => {
|
test.describe("Create and Edit No Code Page view Action", async () => {
|
||||||
@@ -347,14 +361,14 @@ test.describe("Create and Edit Code Action", async () => {
|
|||||||
|
|
||||||
test.describe("Create and Delete Action", async () => {
|
test.describe("Create and Delete Action", async () => {
|
||||||
test.describe.configure({ mode: "serial" });
|
test.describe.configure({ mode: "serial" });
|
||||||
const { email, password, name: username } = users.action[5];
|
const { email, password } = users.action[5];
|
||||||
|
|
||||||
test("Create Action", async ({ page }) => {
|
test("Create Action", async ({ page }) => {
|
||||||
await createNoCodeClickAction(
|
await createNoCodeClickAction(
|
||||||
page,
|
page,
|
||||||
username,
|
// username,
|
||||||
email,
|
// email,
|
||||||
password,
|
// password,
|
||||||
actions.delete.noCode.name,
|
actions.delete.noCode.name,
|
||||||
actions.delete.noCode.description,
|
actions.delete.noCode.description,
|
||||||
actions.delete.noCode.selector
|
actions.delete.noCode.selector
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { finishOnboarding, signUpAndLogin } from "@/playwright/utils/helper";
|
import { finishOnboarding, signUpAndLogin } from "@/playwright/utils/helper";
|
||||||
import { users } from "@/playwright/utils/mock";
|
import { users } from "@/playwright/utils/mock";
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
import { test } from "../../lib/fixtures";
|
||||||
|
|
||||||
const { name, email, password } = users.survey[2];
|
const { name, email, password } = users.survey[2];
|
||||||
|
|
||||||
|
|||||||
87
apps/web/playwright/fixtures/users.ts
Normal file
87
apps/web/playwright/fixtures/users.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { Page } from "playwright";
|
||||||
|
import { expect } from "playwright/test";
|
||||||
|
import { prisma } from "@formbricks/database";
|
||||||
|
|
||||||
|
export const login = async (user: Prisma.UserGetPayload<{ include: { memberships: true } }>, page: Page) => {
|
||||||
|
console.log("logging in", user);
|
||||||
|
await page.goto("/auth/login");
|
||||||
|
|
||||||
|
await expect(page.getByRole("button", { name: "Login with Email" })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Login with Email" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByPlaceholder("work@email.com")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByPlaceholder("work@email.com").fill(user.email);
|
||||||
|
|
||||||
|
await expect(page.getByPlaceholder("*******")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByPlaceholder("*******").click();
|
||||||
|
await page.getByPlaceholder("*******").fill(user.name);
|
||||||
|
await page.getByRole("button", { name: "Login with Email" }).click();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createUserFixture = (
|
||||||
|
user: Prisma.UserGetPayload<{ include: { memberships: true } }>,
|
||||||
|
page: Page
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
login: async () => {
|
||||||
|
await login(user, page);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createUsersFixture = (page: Page) => {
|
||||||
|
return {
|
||||||
|
create: async (params: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
organizationName: string;
|
||||||
|
productName?: string;
|
||||||
|
}) => {
|
||||||
|
const hashedPassword = await bcrypt.hash(params.name, 10);
|
||||||
|
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
name: params.name,
|
||||||
|
email: params.email,
|
||||||
|
password: hashedPassword,
|
||||||
|
memberships: {
|
||||||
|
create: {
|
||||||
|
organization: {
|
||||||
|
create: {
|
||||||
|
name: params.organizationName,
|
||||||
|
billing: {
|
||||||
|
plan: "free",
|
||||||
|
limits: { monthly: { responses: 500, miu: 1000 } },
|
||||||
|
stripeCustomerId: null,
|
||||||
|
periodStart: new Date(),
|
||||||
|
period: "monthly",
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
create: {
|
||||||
|
name: params.productName ?? "My Product",
|
||||||
|
environments: {
|
||||||
|
create: [{ type: "development" }, { type: "production" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { memberships: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("created user", user);
|
||||||
|
|
||||||
|
const userFixture = createUserFixture(user, page);
|
||||||
|
return userFixture;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
import { finishOnboarding, login, replaceEnvironmentIdInHtml, signUpAndLogin } from "./utils/helper";
|
import { finishOnboarding, login, replaceEnvironmentIdInHtml, signUpAndLogin } from "./utils/helper";
|
||||||
import { users } from "./utils/mock";
|
import { users } from "./utils/mock";
|
||||||
|
|
||||||
|
|||||||
13
apps/web/playwright/lib/fixtures.ts
Normal file
13
apps/web/playwright/lib/fixtures.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
import { createUsersFixture } from "../fixtures/users";
|
||||||
|
|
||||||
|
export interface Fixtures {
|
||||||
|
users: ReturnType<typeof createUsersFixture>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test = base.extend<Fixtures>({
|
||||||
|
users: async ({ page }, use) => {
|
||||||
|
const usersFixture = createUsersFixture(page);
|
||||||
|
await use(usersFixture);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
import { signUpAndLogin } from "./utils/helper";
|
import { signUpAndLogin } from "./utils/helper";
|
||||||
import { organizations, users } from "./utils/mock";
|
import { organizations, users } from "./utils/mock";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from "playwright/test";
|
import { expect } from "playwright/test";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
import { finishOnboarding, login, signUpAndLogin, signupUsingInviteToken } from "./utils/helper";
|
import { finishOnboarding, login, signUpAndLogin, signupUsingInviteToken } from "./utils/helper";
|
||||||
import { invites, users } from "./utils/mock";
|
import { invites, users } from "./utils/mock";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
import { users } from "./utils/mock";
|
import { users } from "./utils/mock";
|
||||||
|
|
||||||
const { name, email, password } = users.signup[0];
|
const { name, email, password } = users.signup[0];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { surveys, users } from "@/playwright/utils/mock";
|
import { surveys, users } from "@/playwright/utils/mock";
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
import { createSurvey, finishOnboarding, signUpAndLogin } from "./utils/helper";
|
import { createSurvey, finishOnboarding, signUpAndLogin } from "./utils/helper";
|
||||||
|
|
||||||
test.describe("Survey Create & Submit Response", async () => {
|
test.describe("Survey Create & Submit Response", async () => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const PREVIOUS_RESULTS_CACHE_TAG_KEY = `getPreviousResult-${hashedKey}` as const
|
|||||||
|
|
||||||
// This function is used to get the previous result of the license check from the cache
|
// This function is used to get the previous result of the license check from the cache
|
||||||
// This might seem confusing at first since we only return the default value from this function,
|
// This might seem confusing at first since we only return the default value from this function,
|
||||||
// but since we are using a cache and the cache key is the same, the cache will return the previous result - so this functions as a cache getter
|
// but since we are using a cache and the cache key is the same, the cache will return the previous result - so this function acts as a cache getter
|
||||||
const getPreviousResult = (): Promise<{
|
const getPreviousResult = (): Promise<{
|
||||||
active: boolean | null;
|
active: boolean | null;
|
||||||
lastChecked: Date;
|
lastChecked: Date;
|
||||||
@@ -89,47 +89,86 @@ const fetchLicenseForE2ETesting = async (): Promise<{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getIsEnterpriseEdition = async (): Promise<boolean> => {
|
export const getEnterpriseLicense = async (): Promise<{
|
||||||
|
active: boolean;
|
||||||
|
features: TEnterpriseLicenseFeatures | null;
|
||||||
|
lastChecked: Date;
|
||||||
|
isPendingDowngrade?: boolean;
|
||||||
|
}> => {
|
||||||
if (!ENTERPRISE_LICENSE_KEY || ENTERPRISE_LICENSE_KEY.length === 0) {
|
if (!ENTERPRISE_LICENSE_KEY || ENTERPRISE_LICENSE_KEY.length === 0) {
|
||||||
return false;
|
return {
|
||||||
|
active: false,
|
||||||
|
features: null,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (E2E_TESTING) {
|
if (E2E_TESTING) {
|
||||||
const previousResult = await fetchLicenseForE2ETesting();
|
const previousResult = await fetchLicenseForE2ETesting();
|
||||||
return previousResult && previousResult.active !== null ? previousResult.active : false;
|
// return previousResult && previousResult.active !== null ? previousResult.active : false;
|
||||||
|
return {
|
||||||
|
active: previousResult ? previousResult.active : false,
|
||||||
|
features: previousResult ? previousResult.features : null,
|
||||||
|
lastChecked: previousResult ? previousResult.lastChecked : new Date(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the server responds with a boolean, we return it
|
// if the server responds with a boolean, we return it
|
||||||
// if the server errors, we return null
|
// if the server errors, we return null
|
||||||
// null signifies an error
|
// null signifies an error
|
||||||
const license = await fetchLicense();
|
const license = await fetchLicense();
|
||||||
|
|
||||||
const isValid = license ? license.status === "active" : null;
|
const isValid = license ? license.status === "active" : null;
|
||||||
|
const threeDaysInMillis = 3 * 24 * 60 * 60 * 1000;
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
const previousResult = await getPreviousResult();
|
const previousResult = await getPreviousResult();
|
||||||
|
|
||||||
if (previousResult.active === null) {
|
if (previousResult.active === null) {
|
||||||
if (isValid === null) {
|
if (isValid === null) {
|
||||||
await setPreviousResult({
|
const newResult = {
|
||||||
active: false,
|
active: false,
|
||||||
features: { isMultiOrgEnabled: false },
|
features: { isMultiOrgEnabled: false },
|
||||||
lastChecked: new Date(),
|
lastChecked: new Date(),
|
||||||
});
|
};
|
||||||
return false;
|
|
||||||
|
await setPreviousResult(newResult);
|
||||||
|
return newResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValid !== null && license) {
|
if (isValid !== null && license) {
|
||||||
await setPreviousResult({ active: isValid, features: license.features, lastChecked: new Date() });
|
const newResult = {
|
||||||
return isValid;
|
active: isValid,
|
||||||
|
features: license.features,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await setPreviousResult(newResult);
|
||||||
|
return newResult;
|
||||||
} else {
|
} else {
|
||||||
// if result is undefined -> error
|
// if result is undefined -> error
|
||||||
// if the last check was less than 72 hours, return the previous value:
|
// if the last check was less than 72 hours, return the previous value:
|
||||||
if (new Date().getTime() - previousResult.lastChecked.getTime() <= 3 * 24 * 60 * 60 * 1000) {
|
|
||||||
return previousResult.active !== null ? previousResult.active : false;
|
const elapsedTime = currentTime.getTime() - previousResult.lastChecked.getTime();
|
||||||
|
if (elapsedTime < threeDaysInMillis) {
|
||||||
|
return {
|
||||||
|
active: previousResult.active !== null ? previousResult.active : false,
|
||||||
|
features: previousResult.features,
|
||||||
|
lastChecked: previousResult.lastChecked,
|
||||||
|
isPendingDowngrade: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the last check was more than 72 hours, return false and log the error
|
// Log error only after 72 hours
|
||||||
console.error("Error while checking license: The license check failed");
|
console.error("Error while checking license: The license check failed");
|
||||||
return false;
|
|
||||||
|
return {
|
||||||
|
active: false,
|
||||||
|
features: null,
|
||||||
|
lastChecked: previousResult.lastChecked,
|
||||||
|
isPendingDowngrade: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,14 +178,13 @@ export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures |
|
|||||||
return previousResult.features;
|
return previousResult.features;
|
||||||
} else {
|
} else {
|
||||||
const license = await fetchLicense();
|
const license = await fetchLicense();
|
||||||
if (!license) return null;
|
if (!license || !license.features) return null;
|
||||||
const features = await license.features;
|
return license.features;
|
||||||
return features;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchLicense = async () => {
|
export const fetchLicense = async (): Promise<TEnterpriseLicenseDetails | null> =>
|
||||||
const licenseResult: TEnterpriseLicenseDetails | null = await cache(
|
await cache(
|
||||||
async () => {
|
async () => {
|
||||||
if (!env.ENTERPRISE_LICENSE_KEY) return null;
|
if (!env.ENTERPRISE_LICENSE_KEY) return null;
|
||||||
try {
|
try {
|
||||||
@@ -192,8 +230,6 @@ export const fetchLicense = async () => {
|
|||||||
[`fetchLicense-${hashedKey}`],
|
[`fetchLicense-${hashedKey}`],
|
||||||
{ revalidate: 60 * 60 * 24 }
|
{ revalidate: 60 * 60 * 24 }
|
||||||
)();
|
)();
|
||||||
return licenseResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRemoveInAppBrandingPermission = (organization: TOrganization): boolean => {
|
export const getRemoveInAppBrandingPermission = (organization: TOrganization): boolean => {
|
||||||
if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE;
|
if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE;
|
||||||
@@ -213,7 +249,7 @@ export const getRoleManagementPermission = async (organization: TOrganization):
|
|||||||
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
|
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
|
||||||
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
|
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
|
||||||
);
|
);
|
||||||
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
|
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,13 +259,13 @@ export const getAdvancedTargetingPermission = async (organization: TOrganization
|
|||||||
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
|
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
|
||||||
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
|
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
|
||||||
);
|
);
|
||||||
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
|
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
|
||||||
else return false;
|
else return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBiggerUploadFileSizePermission = async (organization: TOrganization): Promise<boolean> => {
|
export const getBiggerUploadFileSizePermission = async (organization: TOrganization): Promise<boolean> => {
|
||||||
if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE;
|
if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE;
|
||||||
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
|
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,7 +279,7 @@ export const getMultiLanguagePermission = async (organization: TOrganization): P
|
|||||||
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
|
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
|
||||||
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
|
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
|
||||||
);
|
);
|
||||||
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
|
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
45
packages/ui/PendingDowngradeBanner/index.tsx
Normal file
45
packages/ui/PendingDowngradeBanner/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
interface PendingDowngradeBannerProps {
|
||||||
|
lastChecked: Date;
|
||||||
|
active: boolean;
|
||||||
|
isPendingDowngrade: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PendingDowngradeBanner = ({
|
||||||
|
lastChecked,
|
||||||
|
active,
|
||||||
|
isPendingDowngrade,
|
||||||
|
}: PendingDowngradeBannerProps) => {
|
||||||
|
const threeDaysInMillis = 3 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const isLastCheckedWithin72Hours = lastChecked
|
||||||
|
? new Date().getTime() - lastChecked.getTime() < threeDaysInMillis
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const scheduledDowngradeDate = new Date(lastChecked.getTime() + threeDaysInMillis);
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
if (isPendingDowngrade && isLastCheckedWithin72Hours) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="z-40 flex h-5 items-center justify-center bg-orange-800 text-center text-xs text-white">
|
||||||
|
We were unable to verify your license because the license server is unreachable. You will be
|
||||||
|
downgraded to the Community Edition on {scheduledDowngradeDate.toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPendingDowngrade && !isLastCheckedWithin72Hours) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="z-40 flex h-5 items-center justify-center bg-orange-800 text-center text-xs text-white">
|
||||||
|
We were unable to verify your license because the license server is unreachable. You are downgraded
|
||||||
|
to the Community Edition.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { defineConfig, devices } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
// import os from "os";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
* https://github.com/motdotla/dotenv
|
* https://github.com/motdotla/dotenv
|
||||||
@@ -18,6 +20,7 @@ export default defineConfig({
|
|||||||
/* Timeout for each test */
|
/* Timeout for each test */
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
|
// workers: os.cpus().length,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: "html",
|
reporter: "html",
|
||||||
@@ -36,6 +39,7 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
name: "chromium",
|
name: "chromium",
|
||||||
use: { ...devices["Desktop Chrome"] },
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
testMatch: "**/*.spec.ts",
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
|||||||
Reference in New Issue
Block a user