chore: A/B test different upgrade banner in trial (#7953)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Harsh Bhat
2026-05-18 01:33:29 +05:30
committed by GitHub
parent 104b04bdc0
commit 073c28e5e9
19 changed files with 118 additions and 7 deletions
@@ -41,6 +41,7 @@ import { getAccessFlags } from "@/lib/membership/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
import { TrialAlert } from "@/modules/ee/billing/components/trial-alert";
import { TRIAL_BASE_RESPONSE_LIMIT, TrialBannerNew } from "@/modules/ee/billing/components/trial-banner-new";
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
import { ProfileAvatar } from "@/modules/ui/components/avatars";
import { Badge } from "@/modules/ui/components/badge";
@@ -73,6 +74,8 @@ interface NavigationProps {
organizationWorkspacesLimit: number;
isLicenseActive: boolean;
isAccessControlAllowed: boolean;
responseCount: number;
newTrialBannerVariant: string | boolean;
}
export const MainNavigation = ({
@@ -87,6 +90,8 @@ export const MainNavigation = ({
organizationWorkspacesLimit,
isLicenseActive,
isAccessControlAllowed,
responseCount,
newTrialBannerVariant,
}: NavigationProps) => {
const router = useRouter();
const pathname = usePathname();
@@ -582,13 +587,26 @@ export const MainNavigation = ({
)}
{/* Trial Days Remaining */}
{!isCollapsed && isFormbricksCloud && trialDaysRemaining !== null && (
<Link
href={`/workspaces/${workspace.id}/settings/organization/billing`}
className="m-2 block">
<TrialAlert trialDaysRemaining={trialDaysRemaining} size="small" />
</Link>
)}
{!isCollapsed &&
isFormbricksCloud &&
trialDaysRemaining !== null &&
(newTrialBannerVariant === "new-trial-banner" ? (
<TrialBannerNew
trialDaysRemaining={trialDaysRemaining}
planName={organization.billing.stripe?.plan ?? "pro"}
responseCount={responseCount}
responseLimit={organization.billing.limits.monthly.responses}
baseResponseLimit={TRIAL_BASE_RESPONSE_LIMIT}
billingHref={`/workspaces/${workspace.id}/settings/organization/billing`}
hasPaymentMethod={organization.billing.stripe?.hasPaymentMethod}
/>
) : (
<Link
href={`/workspaces/${workspace.id}/settings/organization/billing`}
className="m-2 block">
<TrialAlert trialDaysRemaining={trialDaysRemaining} size="small" />
</Link>
))}
<div className="flex flex-col">
<DropdownMenu onOpenChange={setIsWorkspaceDropdownOpen}>
@@ -4,6 +4,7 @@ import { TopControlBar } from "@/app/(app)/workspaces/[workspaceId]/components/T
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getPublicDomain } from "@/lib/getPublicUrl";
import { getAccessFlags } from "@/lib/membership/utils";
import { getPostHogFeatureFlag } from "@/lib/posthog/get-feature-flag";
import { getTranslate } from "@/lingodotdev/server";
import { getOrganizationWorkspacesLimit } from "@/modules/ee/license-check/lib/utils";
import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-banner";
@@ -37,6 +38,7 @@ export const WorkspaceLayout = async ({ layoutData, children }: WorkspaceLayoutP
const { features, lastChecked, isPendingDowngrade, active, status } = license;
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
const organizationWorkspacesLimit = await getOrganizationWorkspacesLimit(organization.id);
const newTrialBannerVariant = await getPostHogFeatureFlag(user.id, "a-b_navigation_rich-trial-banner");
const isOwnerOrManager = isOwner || isManager;
// Validate that workspace permission exists for members
@@ -71,6 +73,8 @@ export const WorkspaceLayout = async ({ layoutData, children }: WorkspaceLayoutP
organizationWorkspacesLimit={organizationWorkspacesLimit}
isLicenseActive={active}
isAccessControlAllowed={isAccessControlAllowed}
responseCount={responseCount}
newTrialBannerVariant={newTrialBannerVariant}
/>
<div id="mainContent" className="flex flex-1 flex-col overflow-hidden bg-slate-50">
<TopControlBar
+1
View File
@@ -449,6 +449,7 @@ checksums:
common/trial_days_remaining: 914ff3132895e410bf0f862433ccb49e
common/trial_expired: ca9f0532ac40ca427ca1ba4c86454e07
common/trial_one_day_remaining: 2d64d39fca9589c4865357817bcc24d5
common/trial_plan_badge: b7928bd1938c56199e7d8aada43e587e
common/try_again: 33dd8820e743e35a66e6977f69e9d3b5
common/type: f04471a7ddac844b9ad145eb9911ef75
common/unknown_survey: dd8f6985e17ccf19fac1776e18b2c498
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} Tage verbleibend in deiner Testphase",
"trial_expired": "Deine Testphase ist abgelaufen",
"trial_one_day_remaining": "1 Tag verbleibend in deiner Testphase",
"trial_plan_badge": "{plan}-Testversion",
"try_again": "Erneut versuchen",
"type": "Typ",
"unknown_survey": "Unbekannte Umfrage",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} days left in your trial",
"trial_expired": "Your trial has expired",
"trial_one_day_remaining": "1 day left in your trial",
"trial_plan_badge": "{plan} Trial",
"try_again": "Try again",
"type": "Type",
"unknown_survey": "Unknown survey",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} días restantes en tu prueba",
"trial_expired": "Tu prueba ha expirado",
"trial_one_day_remaining": "1 día restante en tu prueba",
"trial_plan_badge": "Prueba de {plan}",
"try_again": "Intentar de nuevo",
"type": "Tipo",
"unknown_survey": "Encuesta desconocida",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} jours restants dans votre période d'essai",
"trial_expired": "Votre période d'essai a expiré",
"trial_one_day_remaining": "1 jour restant dans votre période d'essai",
"trial_plan_badge": "Essai {plan}",
"try_again": "Réessayer",
"type": "Type",
"unknown_survey": "Enquête inconnue",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} nap van hátra a próbaidőszakából",
"trial_expired": "A próbaidőszaka lejárt",
"trial_one_day_remaining": "1 nap van hátra a próbaidőszakából",
"trial_plan_badge": "{plan} próbaverzió",
"try_again": "Próbálja újra",
"type": "Típus",
"unknown_survey": "Ismeretlen kérdőív",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "トライアル期間の残り{count}日",
"trial_expired": "トライアル期間が終了しました",
"trial_one_day_remaining": "トライアル期間の残り1日",
"trial_plan_badge": "{plan}トライアル",
"try_again": "もう一度お試しください",
"type": "種類",
"unknown_survey": "不明なフォーム",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} dagen over in je proefperiode",
"trial_expired": "Je proefperiode is verlopen",
"trial_one_day_remaining": "1 dag over in je proefperiode",
"trial_plan_badge": "{plan} Proefperiode",
"try_again": "Probeer het opnieuw",
"type": "Type",
"unknown_survey": "Onbekende enquête",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} dias restantes no seu período de teste",
"trial_expired": "Seu período de teste expirou",
"trial_one_day_remaining": "1 dia restante no seu período de teste",
"trial_plan_badge": "Teste {plan}",
"try_again": "Tenta de novo",
"type": "Tipo",
"unknown_survey": "Pesquisa desconhecida",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} dias restantes no teu período de teste",
"trial_expired": "O teu período de teste expirou",
"trial_one_day_remaining": "1 dia restante no teu período de teste",
"trial_plan_badge": "Teste {plan}",
"try_again": "Tente novamente",
"type": "Tipo",
"unknown_survey": "Inquérito desconhecido",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} zile rămase în perioada ta de probă",
"trial_expired": "Perioada ta de probă a expirat",
"trial_one_day_remaining": "1 zi rămasă în perioada ta de probă",
"trial_plan_badge": "Perioadă de probă {plan}",
"try_again": "Încearcă din nou",
"type": "Tip",
"unknown_survey": "Chestionar necunoscut",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count, plural, one {Остался # день пробного периода} few {Осталось # дня пробного периода} many {Осталось # дней пробного периода} other {Осталось # дней пробного периода}}",
"trial_expired": "Пробный период истёк",
"trial_one_day_remaining": "Остался 1 день пробного периода",
"trial_plan_badge": "Пробная версия {plan}",
"try_again": "Попробуйте ещё раз",
"type": "Тип",
"unknown_survey": "Неизвестный опрос",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "{count} dagar kvar av din provperiod",
"trial_expired": "Din provperiod har gått ut",
"trial_one_day_remaining": "1 dag kvar av din provperiod",
"trial_plan_badge": "{plan} provperiod",
"try_again": "Försök igen",
"type": "Typ",
"unknown_survey": "Okänd enkät",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "Deneme sürenizde {count} gün kaldı",
"trial_expired": "Deneme süreniz doldu",
"trial_one_day_remaining": "Deneme sürenizde 1 gün kaldı",
"trial_plan_badge": "{plan} Deneme",
"try_again": "Tekrar dene",
"type": "Tür",
"unknown_survey": "Bilinmeyen anket",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "试用期还剩 {count} 天",
"trial_expired": "您的试用期已过期",
"trial_one_day_remaining": "试用期还剩 1 天",
"trial_plan_badge": "{plan} 试用版",
"try_again": "再试一次",
"type": "类型",
"unknown_survey": "未知调查",
+1
View File
@@ -476,6 +476,7 @@
"trial_days_remaining": "試用期剩餘 {count} 天",
"trial_expired": "您的試用期已結束",
"trial_one_day_remaining": "試用期剩餘 1 天",
"trial_plan_badge": "{plan} 試用版",
"try_again": "再試一次",
"type": "類型",
"unknown_survey": "未知問卷",
@@ -0,0 +1,73 @@
"use client";
import Link from "next/link";
import posthog from "posthog-js";
import { useTranslation } from "react-i18next";
import { Button } from "@/modules/ui/components/button";
export const TRIAL_BASE_RESPONSE_LIMIT = 250;
interface TrialBannerNewProps {
trialDaysRemaining: number;
planName: string;
responseCount: number;
responseLimit: number | null;
baseResponseLimit: number;
billingHref: string;
hasPaymentMethod?: boolean;
}
export const TrialBannerNew = ({
trialDaysRemaining,
planName,
responseCount,
responseLimit,
baseResponseLimit,
billingHref,
hasPaymentMethod = false,
}: TrialBannerNewProps) => {
const { t, i18n } = useTranslation();
const locale = i18n.resolvedLanguage ?? i18n.language ?? "en-US";
const effectiveLimit = responseLimit ?? baseResponseLimit;
const progressPercent = Math.min((responseCount / effectiveLimit) * 100, 100);
const planLabel = planName.charAt(0).toUpperCase() + planName.slice(1);
return (
<div className="m-2 rounded-lg border border-slate-200 bg-white p-3 text-sm shadow-sm">
<div className="mb-1 flex items-center gap-2">
<span className="font-semibold text-slate-800">
{trialDaysRemaining > 0
? t("common.trial_days_remaining", { count: trialDaysRemaining })
: t("common.trial_expired")}
</span>
<span className="whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-600">
{t("common.trial_plan_badge", { plan: planLabel })}
</span>
</div>
<p className="mb-2 text-xs text-slate-500">
{responseCount.toLocaleString(locale)} /{" "}
<span className="line-through">{baseResponseLimit.toLocaleString(locale)}</span>{" "}
{effectiveLimit.toLocaleString(locale)} {t("common.responses")}
</p>
<div className="mb-3 h-1.5 w-full overflow-hidden rounded-full bg-slate-100">
<div
className="h-full rounded-full bg-slate-600 transition-all"
style={{ width: `${progressPercent}%` }}
/>
</div>
{!hasPaymentMethod && (
<Button
variant="secondary"
size="sm"
className="w-full"
onClick={() => posthog.capture("main_nav_add_payment_clicked")}>
<Link href={billingHref}>{t("workspace.settings.billing.add_payment_method")}</Link>
</Button>
)}
</div>
);
};