diff --git a/components/Shell.tsx b/components/Shell.tsx
index 3b86d37c96..707d688373 100644
--- a/components/Shell.tsx
+++ b/components/Shell.tsx
@@ -16,6 +16,7 @@ import React, { ReactNode, useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import LicenseBanner from "@ee/components/LicenseBanner";
+import TrialBanner from "@ee/components/TrialBanner";
import HelpMenuItemDynamic from "@ee/lib/intercom/HelpMenuItemDynamic";
import classNames from "@lib/classNames";
@@ -242,6 +243,7 @@ export default function Shell(props: {
))}
+
diff --git a/ee/components/TrialBanner.tsx b/ee/components/TrialBanner.tsx
new file mode 100644
index 0000000000..fc63226c24
--- /dev/null
+++ b/ee/components/TrialBanner.tsx
@@ -0,0 +1,35 @@
+import dayjs from "dayjs";
+
+import { TRIAL_LIMIT_DAYS } from "@lib/config/constants";
+import { useLocale } from "@lib/hooks/useLocale";
+
+import { useMeQuery } from "@components/Shell";
+import Button from "@components/ui/Button";
+
+const TrialBanner = () => {
+ const { t } = useLocale();
+ const query = useMeQuery();
+ const user = query.data;
+
+ if (!user || user.plan !== "TRIAL") return null;
+
+ const trialDaysLeft = dayjs(user.createdDate)
+ .add(TRIAL_LIMIT_DAYS + 1, "day")
+ .diff(dayjs(), "day");
+
+ return (
+
+
{t("trial_days_left", { days: trialDaysLeft })}
+
+
+ );
+};
+
+export default TrialBanner;
diff --git a/ee/lib/stripe/client.ts b/ee/lib/stripe/client.ts
index f326e54fb2..a89840f0f8 100644
--- a/ee/lib/stripe/client.ts
+++ b/ee/lib/stripe/client.ts
@@ -1,4 +1,5 @@
-import { loadStripe, Stripe } from "@stripe/stripe-js";
+import { Stripe } from "@stripe/stripe-js";
+import { loadStripe } from "@stripe/stripe-js/pure";
import { stringify } from "querystring";
import { Maybe } from "@trpc/server";
diff --git a/lib/config/constants.ts b/lib/config/constants.ts
index b686c6f7da..e69f03ed85 100644
--- a/lib/config/constants.ts
+++ b/lib/config/constants.ts
@@ -1,2 +1,4 @@
export const BASE_URL = process.env.BASE_URL || `https://${process.env.VERCEL_URL}`;
+export const WEBSITE_URL = process.env.NEXT_PUBLIC_APP_URL || "https://cal.com";
export const IS_PRODUCTION = process.env.NODE_ENV === "production";
+export const TRIAL_LIMIT_DAYS = 14;
diff --git a/next-i18next.config.js b/next-i18next.config.js
index 6ab76d68aa..4d723d9bc8 100644
--- a/next-i18next.config.js
+++ b/next-i18next.config.js
@@ -7,4 +7,5 @@ module.exports = {
locales: ["en", "fr", "it", "ru", "es", "de", "pt", "ro", "nl", "pt-BR", "es-419", "ko", "ja"],
},
localePath: path.resolve("./public/static/locales"),
+ reloadOnPrerender: process.env.NODE_ENV !== "production",
};
diff --git a/pages/api/cron/downgradeUsers.ts b/pages/api/cron/downgradeUsers.ts
index 0e1a1aa88f..fd60c8d236 100644
--- a/pages/api/cron/downgradeUsers.ts
+++ b/pages/api/cron/downgradeUsers.ts
@@ -1,10 +1,9 @@
import dayjs from "dayjs";
import type { NextApiRequest, NextApiResponse } from "next";
+import { TRIAL_LIMIT_DAYS } from "@lib/config/constants";
import prisma from "@lib/prisma";
-const TRIAL_LIMIT_DAYS = 14;
-
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
diff --git a/pages/api/upgrade.ts b/pages/api/upgrade.ts
new file mode 100644
index 0000000000..e415ce62b6
--- /dev/null
+++ b/pages/api/upgrade.ts
@@ -0,0 +1,50 @@
+import { Prisma } from "@prisma/client";
+import type { NextApiRequest, NextApiResponse } from "next";
+
+import { getSession } from "@lib/auth";
+import { WEBSITE_URL } from "@lib/config/constants";
+import { HttpError as HttpCode } from "@lib/core/http/error";
+import prisma from "@lib/prisma";
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ const session = await getSession({ req });
+ if (!session?.user?.id) {
+ return res.status(401).json({ message: "Not authenticated" });
+ }
+
+ if (req.method !== "GET") {
+ throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
+ }
+
+ const user = await prisma.user.findUnique({
+ rejectOnNotFound: true,
+ where: {
+ id: session.user.id,
+ },
+ select: {
+ email: true,
+ metadata: true,
+ },
+ });
+
+ try {
+ const response = await fetch(`${WEBSITE_URL}/api/upgrade`, {
+ method: "POST",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ stripeCustomerId: (user.metadata as Prisma.JsonObject)?.stripeCustomerId,
+ email: user.email,
+ fromApp: true,
+ }),
+ });
+ const data = await response.json();
+
+ res.redirect(303, data.url);
+ } catch (error) {
+ console.error(`error`, error);
+ res.redirect(303, req.headers.origin || "/");
+ }
+}
diff --git a/pages/auth/login.tsx b/pages/auth/login.tsx
index e77375eff8..69358ee769 100644
--- a/pages/auth/login.tsx
+++ b/pages/auth/login.tsx
@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { ErrorCode, getSession } from "@lib/auth";
+import { WEBSITE_URL } from "@lib/config/constants";
import { useLocale } from "@lib/hooks/useLocale";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -176,7 +177,7 @@ export default function Login({ csrfToken }: inferSSRProps
diff --git a/playwright/lib/globalSetup.ts b/playwright/lib/globalSetup.ts
index 273c6b0345..c5cbcfdd70 100644
--- a/playwright/lib/globalSetup.ts
+++ b/playwright/lib/globalSetup.ts
@@ -26,7 +26,7 @@ async function globalSetup(/* config: FullConfig */) {
await loginAsUser("onboarding", browser);
// await loginAsUser("free-first-hidden", browser);
await loginAsUser("pro", browser);
- // await loginAsUser("trial", browser);
+ await loginAsUser("trial", browser);
await loginAsUser("free", browser);
// await loginAsUser("usa", browser);
// await loginAsUser("teamfree", browser);
diff --git a/playwright/trial.test.ts b/playwright/trial.test.ts
new file mode 100644
index 0000000000..debb80eae5
--- /dev/null
+++ b/playwright/trial.test.ts
@@ -0,0 +1,13 @@
+import { expect, test } from "@playwright/test";
+
+// Using logged in state from globalSteup
+test.use({ storageState: "playwright/artifacts/trialStorageState.json" });
+
+test("Trial banner should be visible to TRIAL users", async ({ page }) => {
+ // Try to go homepage
+ await page.goto("/");
+ // It should redirect you to the event-types page
+ await page.waitForSelector("[data-testid=event-types]");
+
+ await expect(page.locator(`[data-testid=trial-banner]`)).toBeVisible();
+});
diff --git a/public/static/locales/en/common.json b/public/static/locales/en/common.json
index 473fc7942e..f51147fb22 100644
--- a/public/static/locales/en/common.json
+++ b/public/static/locales/en/common.json
@@ -1,4 +1,8 @@
{
+ "trial_days_left": "You have $t(day, {\"count\": {{days}} }) left on your PRO trial",
+ "day": "{{count}} day",
+ "day_plural": "{{count}} days",
+ "upgrade_now": "Upgrade now",
"accept_invitation": "Accept Invitation",
"calcom_explained": "Cal.com is the open source Calendly alternative putting you in control of your own data, workflow and appearance.",
"have_any_questions": "Have questions? We're here to help.",