Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cookies } from "next/headers";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { MainNavigation } from "@/app/(app)/workspaces/[workspaceId]/components/MainNavigation";
import { TopControlBar } from "@/app/(app)/workspaces/[workspaceId]/components/TopControlBar";
Expand All @@ -6,6 +7,8 @@ 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 { TrialEndingWarningModal } from "@/modules/ee/billing/components/trial-ending-warning-modal";
import { TrialResponseWarningModal } from "@/modules/ee/billing/components/trial-response-warning-modal";
import { getOrganizationWorkspacesLimit } from "@/modules/ee/license-check/lib/utils";
import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-banner";
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
Expand All @@ -16,6 +19,40 @@ interface WorkspaceLayoutProps {
children?: React.ReactNode;
}

type TCookieStore = Awaited<ReturnType<typeof cookies>>;

// Response-limit warning for Hobby (free) plan: 200 = 80% of the 250/mo cap, 250 = limit reached.
const getResponseWarningThreshold = (
responseCount: number,
cookieStore: TCookieStore
): "200" | "250" | null => {
if (responseCount >= 250 && !cookieStore.get("trial_warning_shown_250")) {
return "250";
}
if (
responseCount >= 200 &&
!cookieStore.get("trial_warning_shown_200") &&
!cookieStore.get("trial_warning_shown_250")
) {
return "200";
}
return null;
};

// Show the loss-aversion trial-ending modal once on each of the last 3 days of the trial.
const getTrialEndingDaysRemaining = (trialEnd: string | Date, cookieStore: TCookieStore): number | null => {
const MS_PER_DAY = 86_400_000;
const trialEndTime = new Date(trialEnd).getTime();
if (!Number.isFinite(trialEndTime)) {
return null;
}
const daysRemaining = Math.ceil((trialEndTime - Date.now()) / MS_PER_DAY);
if (daysRemaining >= 1 && daysRemaining <= 3 && !cookieStore.get(`trial_ending_shown_${daysRemaining}`)) {
return daysRemaining;
}
return null;
};

export const WorkspaceLayout = async ({ layoutData, children }: WorkspaceLayoutProps) => {
const t = await getTranslate();
const publicDomain = getPublicDomain();
Expand All @@ -37,18 +74,48 @@ 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-v2");
const isTrialing = IS_FORMBRICKS_CLOUD && organization.billing?.stripe?.subscriptionStatus === "trialing";
// Hobby (free) plan only — excludes trial and paid (Pro/Scale) orgs.
const isHobby = IS_FORMBRICKS_CLOUD && organization.billing?.stripe?.plan === "hobby";

const [
organizationWorkspacesLimit,
newTrialBannerVariant,
responseWarningVariant,
trialEndingVariant,
cookieStore,
] = await Promise.all([
getOrganizationWorkspacesLimit(organization.id),
getPostHogFeatureFlag(user.id, "a-b_navigation_rich-trial-banner-v2"),
isHobby ? getPostHogFeatureFlag(user.id, "a-b_workspace_trial-response-warning") : Promise.resolve(null),
isTrialing ? getPostHogFeatureFlag(user.id, "a-b_workspace_trial-ending-warning") : Promise.resolve(null),
isTrialing || isHobby ? cookies() : Promise.resolve(null),
]);
const isOwnerOrManager = isOwner || isManager;

// Validate that workspace permission exists for members
if (isMember && !workspacePermission) {
throw new ResourceNotFoundError(t("common.workspace"), null);
}

// (Hobby response warning and trial-ending target mutually exclusive audiences, so no stacking.)
const responseWarningThreshold =
isHobby && responseWarningVariant === "test" && cookieStore
? getResponseWarningThreshold(responseCount, cookieStore)
: null;

const trialEnd = organization.billing?.stripe?.trialEnd;
const trialEndingDaysRemaining =
isTrialing && trialEndingVariant === "test" && cookieStore && trialEnd
? getTrialEndingDaysRemaining(trialEnd, cookieStore)
: null;

const billingHref = `/workspaces/${workspace.id}/settings/organization/billing`;

return (
<div className="flex h-screen min-h-screen flex-col overflow-hidden">
{IS_FORMBRICKS_CLOUD && (
{/* Hide the limits-reached toast for Hobby users in the response-warning test variant — the modal replaces it with richer copy + CTAs. */}
{IS_FORMBRICKS_CLOUD && !isTrialing && !(isHobby && responseWarningVariant === "test") && (
<LimitsReachedBanner organization={organization} responseCount={responseCount} />
)}

Expand All @@ -61,6 +128,18 @@ export const WorkspaceLayout = async ({ layoutData, children }: WorkspaceLayoutP
status={status}
/>

{responseWarningThreshold && (
<TrialResponseWarningModal
threshold={responseWarningThreshold}
billingHref={billingHref}
responseCount={responseCount}
/>
)}

{trialEndingDaysRemaining && (
<TrialEndingWarningModal daysRemaining={trialEndingDaysRemaining} billingHref={billingHref} />
)}

<div className="flex h-full">
<MainNavigation
organization={organization}
Expand Down
37 changes: 35 additions & 2 deletions apps/web/i18n.lock
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ checksums:
common/add_workspace: 17b0856283b433b848ce9bf4c03c6ae5
common/all: 5e2e93f8e2a5f315b6fecdd0a0e6f55f
common/all_questions: e13634cf510cc6e89b65a6394ea96a21
common/all_roles: 6582ccd0a2349c162a7ae1574cdf76be
common/allow: 3e39cc5940255e6bff0fea95c817dd43
common/allow_users_to_exit_by_clicking_outside_the_survey: 1c09db6e85214f1b1c3d4774c4c5cd56
common/an_unknown_error_occurred_while_deleting_table_items: 06be3fd128aeb51eed4fba9a079ecee2
Expand Down Expand Up @@ -205,7 +206,7 @@ checksums:
common/created: 6045b4ff97fb7dca19ebadfa2e34a700
common/created_at: f98a3c3bc73494df9bbc14e4e47a018c
common/created_by: 6775c2fa7d495fea48f1ad816daea93b
common/customer_success: 2b0c99a5f57e1d16cf0a998f9bb116c4
common/customer_success: 9b8100f7cc9078702012617237f5b884
common/dark_overlay: 173e84b526414dbc70dbf9737e443b60
common/dashboard: c9380ea68c8c76ea451bd9613329a07c
common/dashboards: 4bc47e48559a6b688684dcb7ac4babc9
Expand Down Expand Up @@ -360,7 +361,7 @@ checksums:
common/password: 223a61cf906ab9c40d22612c588dff48
common/paused: edb1f7b7219e1c9b7aa67159090d6991
common/pending_downgrade: d6796fc1d4df21591c69fbbd39ba53ff
common/people_manager: bc7aad9d28d75491cb8024b5cd5172ea
common/people_manager: c1a2f206157ec618f9fe74bf99a06b85
common/person: b6e3064ca6b67285dc1ebb2590d6094f
common/phone: b9537ee90fc5b0116942e0af29d926cc
common/photo_by: 3b96aa11f830dc89d6975ccbc93ad359
Expand Down Expand Up @@ -1753,6 +1754,7 @@ checksums:
workspace/app-connection/app_connection_description: dde226414bd2265cbd0daf6635efcfdd
workspace/app-connection/cache_update_delay_description: 3368e4a8090b7684117a16c94f0c409c
workspace/app-connection/cache_update_delay_title: 60e4a0fcfbd8850bddf29b5c3f59550c
workspace/app-connection/connect_with_ai: 5373f6dd1fc23c2052b88868189187b3
workspace/app-connection/environment_id_legacy: d5c701874d34b4591e780755f7ac7a58
workspace/app-connection/environment_id_legacy_alert: 09ac96821ff99fad4590c661503fa0cd
workspace/app-connection/environment_id_legacy_alert_link: 25c529078a115d1ff044a321dd8ee01b
Expand All @@ -1767,6 +1769,7 @@ checksums:
workspace/app-connection/sdk_connection_details_description: 2d6824466039672fa002d72da95b4637
workspace/app-connection/setup_alert_description: 6d676044d01dc2147731ffab7df6c259
workspace/app-connection/setup_alert_title: 9561cca2b391e0df81e8a982921ff2bb
workspace/app-connection/view_code_snippet: b3c332e0e715f01d360a66ea993c4224
workspace/app-connection/webapp_url: d64d8cc3c4c4ecce780d94755f7e4de9
workspace/app-connection/workspace_id: 49141af65970ea79e22ecedb97ceb2e4
workspace/app-connection/workspace_id_description: 77e5219be241e9973741f138787ccbb8
Expand Down Expand Up @@ -2257,6 +2260,17 @@ checksums:
workspace/settings/billing/dynamic_pricing_per_unit: 67e4d330b97b8b2e089558ea8148d708
workspace/settings/billing/dynamic_pricing_unlimited: 1762c0cfba36e1229b5cb5f22c08aeb8
workspace/settings/billing/failed_to_start_trial: 43e28223f51af382042b3a753d9e4380
workspace/settings/billing/go_to_billing: cd3ec8ecd7af2e724b44a61b29517ee7
workspace/settings/billing/hobby_confirm_description: f9fc023eb49423eef508bcc98fcd9e5c
workspace/settings/billing/hobby_confirm_downgrade: 77a35281775a0e2bac9c27ae0d28e4bf
workspace/settings/billing/hobby_confirm_feature_ai: a780f0b9c8e8ce9bfc1612e4774f3977
workspace/settings/billing/hobby_confirm_feature_branding: a5c71d43cd3ed25e6e48bca64e8ffc9f
workspace/settings/billing/hobby_confirm_feature_contacts: 6d8e29d34939ed98f1fd6694345f111b
workspace/settings/billing/hobby_confirm_feature_responses: d1e6c1d83f5e57cbae2a09e6a818a25d
workspace/settings/billing/hobby_confirm_feature_workspaces: 02a34669419ed7f30f728980f54d42ef
workspace/settings/billing/hobby_confirm_start_trial: 56dc65bc46b1fddfba62c5f4bf482daf
workspace/settings/billing/hobby_confirm_title: 6d5cd13628a7887711fd0c29f1123652

workspace/settings/billing/keep_current_plan: 57ac15ffa2c29ac364dd405669eeb7f6
workspace/settings/billing/manage_billing_details: 40448f0b5ed4b3bb1d864ba6e1bb6a3b
workspace/settings/billing/monthly: 818f1192e32bb855597f930d3e78806e
Expand Down Expand Up @@ -2335,7 +2349,16 @@ checksums:
workspace/settings/billing/this_includes: 127e0fe104f47886b54106a057a6b26f
workspace/settings/billing/trial_alert_description: e8c20d27d8cd41690db0373463c2eacb
workspace/settings/billing/trial_already_used: 5433347ff7647fe0aba0fe91a44560ba
workspace/settings/billing/trial_ending_add_payment_method: 38ad2a7f6bc599bf596eab394b379c02
workspace/settings/billing/trial_ending_description: 8be0ee811be739704ce92391593ca544
workspace/settings/billing/trial_ending_title: 3c2b88e6693450a7f38255076414eed5
workspace/settings/billing/trial_payment_method_added_description: 917727553a379d52b8a9c8ebce370061
workspace/settings/billing/trial_warning_200_description: b62a8996f7002340e1ed89e37ca713d2
workspace/settings/billing/trial_warning_200_title: 804fae1197fba14b15e2d697e62f34cd
workspace/settings/billing/trial_warning_250_description: 8142e4b105b47443895a315e4b4a156e
workspace/settings/billing/trial_warning_250_title: 93d34846276f30d6b394a8a3a626a291
workspace/settings/billing/trial_warning_add_payment_method: 38ad2a7f6bc599bf596eab394b379c02
workspace/settings/billing/trial_warning_remind_me_later: a12f38fb4352c31ca0d43c05303b7257
workspace/settings/billing/unlimited_responses: 25bd1cd99bc08c66b8d7d3380b2812e1
workspace/settings/billing/unlimited_workspaces: f7433bc693ee6d177e76509277f5c173
workspace/settings/billing/upgrade: 63c3b52882e0d779859307d672c178c2
Expand Down Expand Up @@ -2619,6 +2642,7 @@ checksums:
workspace/surveys/ai_create/choose_template: dc3f22a8178d5aef8bdac9d3fc7cc268
workspace/surveys/ai_create/create: 757ccd28dd533ff3a933355273c1e32a
workspace/surveys/ai_create/create_with_ai: 5d8c659f559f1343bb23fd191a6556ad
workspace/surveys/ai_create/create_with_ai_description: 7aa3fe0237ead1066e01f3e1abd7c0ad
workspace/surveys/ai_create/creating: c949fe6aa47b326f345b405fe9c4d6e7
workspace/surveys/ai_create/dialog_description: 2c93d5a3152989234ed3819308037a53
workspace/surveys/ai_create/dialog_title: eefbf4e47f6df699e89f4996a5f925f8
Expand Down Expand Up @@ -3225,6 +3249,15 @@ checksums:
workspace/surveys/edit/zip: 228fedb7d75304d2a3fcd9f1d1aaccee
workspace/surveys/error_deleting_survey: 5a113f1cc0a430b54b21dd1fd4e65e00
workspace/surveys/error_updating_status: f163ee51d64903a0c994d39008bf7fe4
workspace/surveys/featured_templates/churn_description: 095b315526140f3633b3da354b766ed4
workspace/surveys/featured_templates/churn_title: 5ecbb56a7bc54e945fff08fed017a545
workspace/surveys/featured_templates/csat_description: 0e64d5594f961e5070a95f715594549e
workspace/surveys/featured_templates/csat_title: fdfc1dc6214cce661dcdc32a71d80337
workspace/surveys/featured_templates/hide: 6820df416c78abdd8a71e576345856a9
workspace/surveys/featured_templates/nps_description: a0967386c2e53a3df5a165ff16a8fbda
workspace/surveys/featured_templates/nps_title: 94ef35c2e6ed7061f8a533c9e61b155b
workspace/surveys/featured_templates/see_all_templates: 33e04d0dca427418ed386bb9b38a6443
workspace/surveys/featured_templates/templates_for: 9eb1a1e51222f809c980910dfe7424ef
workspace/surveys/filter/complete_and_partial_responses: 3c272e4b148421da0027c35562b969db
workspace/surveys/filter/complete_responses: cd495765bf63f5bfdb341eadc4f60ac7
workspace/surveys/filter/partial_responses: cc88893d0defaf64f46f17a1cb8c886d
Expand Down
40 changes: 38 additions & 2 deletions apps/web/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"add_workspace": "Workspace hinzufügen",
"all": "Alle",
"all_questions": "Alle Fragen",
"all_roles": "Alle Rollen",
"allow": "erlauben",
"allow_users_to_exit_by_clicking_outside_the_survey": "Erlaube Nutzern, die Umfrage zu verlassen, indem sie außerhalb klicken",
"an_unknown_error_occurred_while_deleting_table_items": "Beim Löschen von {type}s ist ein unbekannter Fehler aufgetreten",
Expand Down Expand Up @@ -234,7 +235,7 @@
"created": "Erstellt",
"created_at": "Erstellt am",
"created_by": "Erstellt von",
"customer_success": "Kundenerfolg",
"customer_success": "Kundenerlebnis",
"dark_overlay": "Dunkle Überlagerung",
"dashboard": "Dashboard",
"dashboards": "Dashboards",
Expand Down Expand Up @@ -389,7 +390,7 @@
"password": "Passwort",
"paused": "Pausiert",
"pending_downgrade": "Ausstehende Herabstufung",
"people_manager": "Personalleiter",
"people_manager": "Mitarbeitererlebnis",
"person": "Person",
"phone": "Telefon",
"photo_by": "Foto von",
Expand Down Expand Up @@ -1823,6 +1824,7 @@
"app_connection_description": "Verbinde deine App oder Website mit Formbricks.",
"cache_update_delay_description": "Wenn du Aktualisierungen an Umfragen, Kontakten, Aktionen oder anderen Daten vornimmst, kann es bis zu 1 Minute dauern, bis diese Änderungen in deiner lokalen App mit dem Formbricks SDK sichtbar werden.",
"cache_update_delay_title": "Änderungen werden nach ~1 Minute durch Caching übernommen",
"connect_with_ai": "Mit KI verbinden",
"environment_id_legacy": "Umgebungs-ID (veraltet)",
"environment_id_legacy_alert": "Deine bestehende SDK-Konfiguration verwendet möglicherweise noch eine veraltete Umgebungs-ID.",
"environment_id_legacy_alert_link": "Erfahre, warum und wie du migrieren kannst.",
Expand All @@ -1837,6 +1839,7 @@
"sdk_connection_details_description": "Deine einzigartige Workspace-ID und SDK-Verbindungs-URL zur Integration von Formbricks in deine Anwendung.",
"setup_alert_description": "Folge dieser Schritt-für-Schritt-Anleitung, um deine App oder Website in unter 5 Minuten zu verbinden.",
"setup_alert_title": "So verbindest du dich",
"view_code_snippet": "Code-Snippet anzeigen",
"webapp_url": "SDK-Verbindungs-URL",
"workspace_id": "Deine Workspace-ID",
"workspace_id_description": "Diese ID identifiziert diesen Formbricks-Workspace eindeutig."
Expand Down Expand Up @@ -2354,6 +2357,17 @@
"dynamic_pricing_per_unit": "Pro Einheit",
"dynamic_pricing_unlimited": "∞",
"failed_to_start_trial": "Testphase konnte nicht gestartet werden. Bitte versuche es erneut.",
"go_to_billing": "Zur Abrechnung",
"hobby_confirm_description": "Teste alle Funktionen risikofrei. Keine Kreditkarte erforderlich.",
"hobby_confirm_downgrade": "Auf Hobby herabstufen",
"hobby_confirm_feature_ai": "KI-Funktionalität",
"hobby_confirm_feature_branding": "Branding entfernen",
"hobby_confirm_feature_contacts": "Kontakterkennung & Targeting",
"hobby_confirm_feature_responses": "250 Antworten / Monat",
"hobby_confirm_feature_workspaces": "1 Workspace",
"hobby_confirm_start_trial": "Mit Testversion fortfahren",
"hobby_confirm_title": "Bist du sicher?",

"keep_current_plan": "Aktuellen Plan beibehalten",
"manage_billing_details": "Kartendaten & Rechnungen verwalten",
"monthly": "Monatlich",
Expand Down Expand Up @@ -2432,7 +2446,16 @@
"this_includes": "Das beinhaltet",
"trial_alert_description": "Füge eine Zahlungsmethode hinzu, um weiterhin Zugriff auf alle Funktionen zu haben.",
"trial_already_used": "Für diese E-Mail-Adresse wurde bereits eine kostenlose Testphase genutzt. Bitte wechsle stattdessen zu einem kostenpflichtigen Tarif.",
"trial_ending_add_payment_method": "Zahlungsmethode hinzufügen",
"trial_ending_description": "Wenn die Testphase endet, verlierst du den Zugriff auf alles, was du mit Pro eingerichtet hast:",
"trial_ending_title": "{count, plural, one {Nur noch # Tag in deiner Testphase} other {Nur noch # Tage in deiner Testphase}}",
"trial_payment_method_added_description": "Alles bereit! Dein Pro-Plan läuft nach Ende der Testphase automatisch weiter.",
"trial_warning_200_description": "Du hast 200 Antworten gesammelt. Sobald du 250 erreichst, akzeptieren deine Umfragen keine neuen Antworten mehr bis zum Ende der 30-Tage-Periode.",
"trial_warning_200_title": "Du hast 80 % deines Antwortlimits erreicht",
"trial_warning_250_description": "Du hast 250 Antworten gesammelt. Ab jetzt akzeptieren deine Umfragen keine neuen Antworten mehr, bis die 30-Tage-Periode endet.",
"trial_warning_250_title": "Du hast dein Limit erreicht",
"trial_warning_add_payment_method": "Zahlungsmethode hinzufügen",
"trial_warning_remind_me_later": "Später erinnern",
"unlimited_responses": "Unbegrenzte Antworten",
"unlimited_workspaces": "Unbegrenzte Workspaces",
"upgrade": "Upgrade",
Expand Down Expand Up @@ -2734,6 +2757,7 @@
"choose_template": "Wähle eine Vorlage",
"create": "Erstellen",
"create_with_ai": "Mit KI erstellen",
"create_with_ai_description": "Beschreibe, was du brauchst, und erhalte einen Umfrage-Entwurf.",
"creating": "Wird erstellt...",
"dialog_description": "Formbricks erstellt einen Entwurf, den du vor der Veröffentlichung bearbeiten kannst.",
"dialog_title": "Erstelle deine Umfrage mit KI",
Expand Down Expand Up @@ -3347,6 +3371,17 @@
},
"error_deleting_survey": "Beim Löschen der Umfrage ist ein Fehler aufgetreten",
"error_updating_status": "Beim Aktualisieren des Umfragestatus ist ein Fehler aufgetreten",
"featured_templates": {
"churn_description": "Finde heraus, warum Leute ihre Abos kündigen.",
"churn_title": "Churn-Umfrage",
"csat_description": "Miss den Customer Satisfaction Score (1-5)",
"csat_title": "CSAT",
"hide": "Vorlagen ausblenden",
"nps_description": "Miss den Net-Promoter-Score (0-10)",
"nps_title": "NPS-Umfrage",
"see_all_templates": "Alle Vorlagen ansehen",
"templates_for": "Vorlagen für"
},
"filter": {
"complete_and_partial_responses": "Vollständige und teilweise Antworten",
"complete_responses": "Vollständige Antworten",
Expand Down Expand Up @@ -3708,6 +3743,7 @@
"csv_data_preview": "Datenvorschau",
"csv_empty_column_headers": "Die CSV-Datei enthält leere Spaltenüberschriften. Alle Spalten müssen einen Namen haben.",
"csv_file_too_large": "Die CSV-Datei ist zu groß. Die maximale Größe beträgt 15MB.",

"csv_files_only": "Nur CSV-Dateien",
"csv_first_value": "Beispiel: {value}",
"csv_fixed_value_action": "Festen Wert festlegen…",
Expand Down
Loading
Loading