feat: implement trial days remaining alert in billing components (#7474)

This commit is contained in:
Johannes
2026-03-13 08:38:43 -07:00
committed by GitHub
parent bddcec0466
commit 2dc5c50f4d
27 changed files with 558 additions and 121 deletions

View File

@@ -28,6 +28,7 @@ import FBLogo from "@/images/formbricks-wordmark.svg";
import { cn } from "@/lib/cn";
import { getAccessFlags } from "@/lib/membership/utils";
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
import { TrialAlert } from "@/modules/ee/billing/components/trial-alert";
import { getLatestStableFbReleaseAction } from "@/modules/projects/settings/(setup)/app-connection/actions";
import { ProfileAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
@@ -167,6 +168,20 @@ export const MainNavigation = ({
if (isOwnerOrManager) loadReleases();
}, [isOwnerOrManager]);
const trialDaysRemaining = useMemo(() => {
if (!isFormbricksCloud || organization.billing?.stripe?.subscriptionStatus !== "trialing") return null;
const trialEnd = organization.billing.stripe.trialEnd;
if (!trialEnd) return null;
const ts = new Date(trialEnd).getTime();
if (!Number.isFinite(ts)) return null;
const msPerDay = 86_400_000;
return Math.ceil((ts - Date.now()) / msPerDay);
}, [
isFormbricksCloud,
organization.billing?.stripe?.subscriptionStatus,
organization.billing?.stripe?.trialEnd,
]);
const mainNavigationLink = `/environments/${environment.id}/${isBilling ? "settings/billing/" : "surveys/"}`;
return (
@@ -241,6 +256,13 @@ export const MainNavigation = ({
</Link>
)}
{/* Trial Days Remaining */}
{!isCollapsed && isFormbricksCloud && trialDaysRemaining !== null && (
<Link href={`/environments/${environment.id}/settings/billing`} className="m-2 block">
<TrialAlert trialDaysRemaining={trialDaysRemaining} size="small" />
</Link>
)}
{/* User Switch */}
<div className="flex items-center">
<DropdownMenu>

View File

@@ -407,6 +407,9 @@ checksums:
common/title: 344e64395eaff6822a57d18623853e1a
common/top_left: aa61bb29b56df3e046b6d68d89ee8986
common/top_right: 241f95c923846911aaf13af6109333e5
common/trial_days_remaining: 914ff3132895e410bf0f862433ccb49e
common/trial_expired: ca9f0532ac40ca427ca1ba4c86454e07
common/trial_one_day_remaining: 2d64d39fca9589c4865357817bcc24d5
common/try_again: 33dd8820e743e35a66e6977f69e9d3b5
common/type: f04471a7ddac844b9ad145eb9911ef75
common/unknown_survey: dd8f6985e17ccf19fac1776e18b2c498
@@ -913,6 +916,7 @@ checksums:
environments/settings/api_keys/add_api_key: 1c11117b1d4665ccdeb68530381c6a9d
environments/settings/api_keys/add_permission: 4f0481d26a32aef6137ee6f18aaf8e89
environments/settings/api_keys/api_keys_description: 42c2d587834d54f124b9541b32ff7133
environments/settings/billing/add_payment_method: 38ad2a7f6bc599bf596eab394b379c02
environments/settings/billing/cancelling: 6e46e789720395bfa1e3a4b3b1519634
environments/settings/billing/failed_to_start_trial: 43e28223f51af382042b3a753d9e4380
environments/settings/billing/manage_subscription: b83a75127b8eabc21dfa1e0f7104db56
@@ -937,14 +941,20 @@ checksums:
environments/settings/billing/stripe_setup_incomplete_description: 9f28a542729cc719bca2ca08e7406284
environments/settings/billing/subscription: ba9f3675e18987d067d48533c8897343
environments/settings/billing/subscription_description: b03618508e576666198d4adf3c2cb9a9
environments/settings/billing/trial_alert_description: aba3076cc6814cc6128d425d3d1957e8
environments/settings/billing/trial_already_used: 5433347ff7647fe0aba0fe91a44560ba
environments/settings/billing/trial_feature_api_access: d7aabb2de18beb5bd30c274cd768a2a9
environments/settings/billing/trial_feature_collaboration: a43509fffe319e14d69a981ef2791517
environments/settings/billing/trial_feature_quotas: 3a67818b3901bdaa72abc62db72ab170
environments/settings/billing/trial_feature_webhooks: 8d7f034e006b2fe0eb8fa9b8f1abef51
environments/settings/billing/trial_feature_whitelabel: 624a7aeca6a0fa65935c63fd7a8e9638
environments/settings/billing/trial_feature_api_access: 8c6d03728c3d9470616eb5cee5f9f65d
environments/settings/billing/trial_feature_attribute_segmentation: 90087da973ae48e32ec6d863516fc8c9
environments/settings/billing/trial_feature_contact_segment_management: 27f17a039ebed6413811ab3a461db2f4
environments/settings/billing/trial_feature_email_followups: 0cc02dc14aa28ce94ca6153c306924e5
environments/settings/billing/trial_feature_hide_branding: b8dbcb24e50e0eb4aeb0c97891cac61d
environments/settings/billing/trial_feature_mobile_sdks: 0963480a27df49657c1b7507adec9a06
environments/settings/billing/trial_feature_respondent_identification: a82e24ab4c27c5e485326678d9b7bd79
environments/settings/billing/trial_feature_unlimited_seats: a3257d5b6a23bfbc4b7fd1108087a823
environments/settings/billing/trial_feature_webhooks: 5ead39fba97fbd37835a476ee67fdd94
environments/settings/billing/trial_no_credit_card: 01c70aa6e1001815a9a11951394923ca
environments/settings/billing/trial_title: 23d0d2cbe306ae0f784b8289bf66a2c7
environments/settings/billing/trial_payment_method_added_description: 872a5c557f56bafc9b7ec4895f9c33e8
environments/settings/billing/trial_title: f2c3791c1fb2970617ec0f2d243a931b
environments/settings/billing/unlimited_responses: 25bd1cd99bc08c66b8d7d3380b2812e1
environments/settings/billing/unlimited_workspaces: f7433bc693ee6d177e76509277f5c173
environments/settings/billing/upgrade: 63c3b52882e0d779859307d672c178c2

View File

@@ -434,6 +434,9 @@
"title": "Titel",
"top_left": "Oben links",
"top_right": "Oben rechts",
"trial_days_remaining": "Noch {count} Tage in deiner Testphase",
"trial_expired": "Deine Testphase ist abgelaufen",
"trial_one_day_remaining": "Noch 1 Tag in deiner Testphase",
"try_again": "Versuch's nochmal",
"type": "Typ",
"unknown_survey": "Unbekannte Umfrage",
@@ -968,6 +971,7 @@
"api_keys_description": "Verwalte API-Schlüssel, um auf die Formbricks-Management-APIs zuzugreifen"
},
"billing": {
"add_payment_method": "Zahlungsmethode hinzufügen",
"cancelling": "Wird storniert",
"failed_to_start_trial": "Die Testversion konnte nicht gestartet werden. Bitte versuche es erneut.",
"manage_subscription": "Abonnement verwalten",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "Die Abrechnungseinrichtung war nicht erfolgreich. Bitte versuche es erneut, um Dein Abo zu aktivieren.",
"subscription": "Abonnement",
"subscription_description": "Verwalte Dein Abonnement und behalte Deine Nutzung im Blick",
"trial_alert_description": "Füge eine Zahlungsmethode hinzu, um weiterhin Zugriff auf alle Funktionen zu behalten.",
"trial_already_used": "Für diese E-Mail-Adresse wurde bereits eine kostenlose Testversion genutzt. Bitte upgraden Sie stattdessen auf einen kostenpflichtigen Plan.",
"trial_feature_api_access": "Vollen API-Zugriff erhalten",
"trial_feature_collaboration": "Alle Team- und Kollaborationsfunktionen",
"trial_feature_quotas": "Kontingente verwalten",
"trial_feature_webhooks": "Benutzerdefinierte Webhooks einrichten",
"trial_feature_whitelabel": "Vollständig white-labeled Umfragen",
"trial_feature_api_access": "API-Zugriff",
"trial_feature_attribute_segmentation": "Attributbasierte Segmentierung",
"trial_feature_contact_segment_management": "Kontakt- & Segmentverwaltung",
"trial_feature_email_followups": "E-Mail-Nachfassaktionen",
"trial_feature_hide_branding": "Formbricks-Branding ausblenden",
"trial_feature_mobile_sdks": "iOS & Android SDKs",
"trial_feature_respondent_identification": "Befragten-Identifikation",
"trial_feature_unlimited_seats": "Unbegrenzte Benutzerplätze",
"trial_feature_webhooks": "Individuelle Webhooks",
"trial_no_credit_card": "14 Tage Testversion, keine Kreditkarte erforderlich",
"trial_title": "Pro-Funktionen kostenlos testen!",
"trial_payment_method_added_description": "Alles bereit! Dein Pro-Tarif läuft nach Ende der Testphase automatisch weiter.",
"trial_title": "Hol dir Formbricks Pro kostenlos!",
"unlimited_responses": "Unbegrenzte Antworten",
"unlimited_workspaces": "Unbegrenzte Projekte",
"upgrade": "Upgrade",

View File

@@ -434,6 +434,9 @@
"title": "Title",
"top_left": "Top Left",
"top_right": "Top Right",
"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",
"try_again": "Try again",
"type": "Type",
"unknown_survey": "Unknown survey",
@@ -968,6 +971,7 @@
"api_keys_description": "Manage API keys to access Formbricks management APIs"
},
"billing": {
"add_payment_method": "Add payment method",
"cancelling": "Cancelling",
"failed_to_start_trial": "Failed to start trial. Please try again.",
"manage_subscription": "Manage subscription",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "Billing setup did not complete successfully. Please retry to activate your subscription.",
"subscription": "Subscription",
"subscription_description": "Manage your subscription plan and monitor your usage",
"trial_alert_description": "Add a payment method to keep access to all features.",
"trial_already_used": "A free trial has already been used for this email address. Please upgrade to a paid plan instead.",
"trial_feature_api_access": "Get full API access",
"trial_feature_collaboration": "All team & collaboration features",
"trial_feature_quotas": "Manage quotas",
"trial_feature_webhooks": "Setup custom webhooks",
"trial_feature_whitelabel": "Fully white-labeled surveys",
"trial_feature_api_access": "API Access",
"trial_feature_attribute_segmentation": "Attribute-based Segmentation",
"trial_feature_contact_segment_management": "Contact & Segment Management",
"trial_feature_email_followups": "Email Follow-ups",
"trial_feature_hide_branding": "Hide Formbricks Branding",
"trial_feature_mobile_sdks": "iOS & Android SDKs",
"trial_feature_respondent_identification": "Respondent Identification",
"trial_feature_unlimited_seats": "Unlimited Seats",
"trial_feature_webhooks": "Custom Webhooks",
"trial_no_credit_card": "14 days trial, no credit card required",
"trial_title": "Try Pro features for free!",
"trial_payment_method_added_description": "You're all set! Your Pro plan will continue automatically after the trial ends.",
"trial_title": "Get Formbricks Pro for free!",
"unlimited_responses": "Unlimited Responses",
"unlimited_workspaces": "Unlimited Workspaces",
"upgrade": "Upgrade",

View File

@@ -434,6 +434,9 @@
"title": "Título",
"top_left": "Superior izquierda",
"top_right": "Superior derecha",
"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",
"try_again": "Intentar de nuevo",
"type": "Tipo",
"unknown_survey": "Encuesta desconocida",
@@ -968,6 +971,7 @@
"api_keys_description": "Gestiona las claves API para acceder a las APIs de gestión de Formbricks"
},
"billing": {
"add_payment_method": "Añadir método de pago",
"cancelling": "Cancelando",
"failed_to_start_trial": "No se pudo iniciar la prueba. Por favor, inténtalo de nuevo.",
"manage_subscription": "Gestionar suscripción",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "La configuración de facturación no se completó correctamente. Por favor, vuelve a intentarlo para activar tu suscripción.",
"subscription": "Suscripción",
"subscription_description": "Gestiona tu plan de suscripción y monitorea tu uso",
"trial_alert_description": "Añade un método de pago para mantener el acceso a todas las funciones.",
"trial_already_used": "Ya se ha utilizado una prueba gratuita para esta dirección de correo electrónico. Por favor, actualiza a un plan de pago.",
"trial_feature_api_access": "Acceso completo a la API",
"trial_feature_collaboration": "Todas las funciones de equipo y colaboración",
"trial_feature_quotas": "Gestionar cuotas",
"trial_feature_webhooks": "Configurar webhooks personalizados",
"trial_feature_whitelabel": "Encuestas totalmente personalizadas",
"trial_feature_api_access": "Acceso a la API",
"trial_feature_attribute_segmentation": "Segmentación basada en atributos",
"trial_feature_contact_segment_management": "Gestión de contactos y segmentos",
"trial_feature_email_followups": "Seguimientos por correo electrónico",
"trial_feature_hide_branding": "Ocultar la marca Formbricks",
"trial_feature_mobile_sdks": "SDKs para iOS y Android",
"trial_feature_respondent_identification": "Identificación de encuestados",
"trial_feature_unlimited_seats": "Asientos ilimitados",
"trial_feature_webhooks": "Webhooks personalizados",
"trial_no_credit_card": "Prueba de 14 días, sin tarjeta de crédito",
"trial_title": "¡Prueba las funciones Pro gratis!",
"trial_payment_method_added_description": "¡Todo listo! Tu plan Pro continuará automáticamente cuando termine el periodo de prueba.",
"trial_title": "¡Consigue Formbricks Pro gratis!",
"unlimited_responses": "Respuestas ilimitadas",
"unlimited_workspaces": "Proyectos ilimitados",
"upgrade": "Actualizar",

View File

@@ -434,6 +434,9 @@
"title": "Titre",
"top_left": "En haut à gauche",
"top_right": "En haut à droite",
"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",
"try_again": "Réessayer",
"type": "Type",
"unknown_survey": "Enquête inconnue",
@@ -968,6 +971,7 @@
"api_keys_description": "Les clés d'API permettent d'accéder aux API de gestion de Formbricks."
},
"billing": {
"add_payment_method": "Ajouter un moyen de paiement",
"cancelling": "Annulation en cours",
"failed_to_start_trial": "Échec du démarrage de l'essai. Réessaye.",
"manage_subscription": "Gérer l'abonnement",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "La configuration de la facturation na pas abouti. Merci de réessayer pour activer ton abonnement.",
"subscription": "Abonnement",
"subscription_description": "Gère ton abonnement et surveille ta consommation",
"trial_alert_description": "Ajoute un moyen de paiement pour conserver l'accès à toutes les fonctionnalités.",
"trial_already_used": "Un essai gratuit a déjà été utilisé pour cette adresse e-mail. Passe plutôt à un plan payant.",
"trial_feature_api_access": "Accès complet à l'API",
"trial_feature_collaboration": "Toutes les fonctionnalités d'équipe et de collaboration",
"trial_feature_quotas": "Gère les quotas",
"trial_feature_webhooks": "Configure des webhooks personnalisés",
"trial_feature_whitelabel": "Enquêtes entièrement en marque blanche",
"trial_feature_api_access": "Accès API",
"trial_feature_attribute_segmentation": "Segmentation basée sur les attributs",
"trial_feature_contact_segment_management": "Gestion des contacts et segments",
"trial_feature_email_followups": "Relances par e-mail",
"trial_feature_hide_branding": "Masquer l'image de marque Formbricks",
"trial_feature_mobile_sdks": "SDKs iOS et Android",
"trial_feature_respondent_identification": "Identification des répondants",
"trial_feature_unlimited_seats": "Places illimitées",
"trial_feature_webhooks": "Webhooks personnalisés",
"trial_no_credit_card": "Essai de 14 jours, aucune carte bancaire requise",
"trial_title": "Essaie les fonctionnalités Pro gratuitement !",
"trial_payment_method_added_description": "Tout est prêt ! Votre abonnement Pro se poursuivra automatiquement après la fin de la période d'essai.",
"trial_title": "Obtenez Formbricks Pro gratuitement !",
"unlimited_responses": "Réponses illimitées",
"unlimited_workspaces": "Projets illimités",
"upgrade": "Mise à niveau",

View File

@@ -434,6 +434,9 @@
"title": "Cím",
"top_left": "Balra fent",
"top_right": "Jobbra fent",
"trial_days_remaining": "{count} nap van hátra a próbaidőszakból",
"trial_expired": "A próbaidőszak lejárt",
"trial_one_day_remaining": "1 nap van hátra a próbaidőszakból",
"try_again": "Próbálja újra",
"type": "Típus",
"unknown_survey": "Ismeretlen kérdőív",
@@ -968,6 +971,7 @@
"api_keys_description": "API-kulcsok kezelése a Formbricks kezelő API-jaihoz való hozzáféréshez"
},
"billing": {
"add_payment_method": "Fizetési mód hozzáadása",
"cancelling": "Lemondás folyamatban",
"failed_to_start_trial": "A próbaidőszak indítása sikertelen. Kérjük, próbálja meg újra.",
"manage_subscription": "Előfizetés kezelése",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "A számlázás beállítása nem sikerült teljesen. Aktiválja előfizetését az újrapróbálkozással.",
"subscription": "Előfizetés",
"subscription_description": "Kezelje előfizetését és kövesse nyomon a használatot",
"trial_alert_description": "Adjon hozzá fizetési módot, hogy megtarthassa a hozzáférést az összes funkcióhoz.",
"trial_already_used": "Ehhez az e-mail címhez már igénybe vettek ingyenes próbaidőszakot. Kérjük, válasszon helyette fizetős csomagot.",
"trial_feature_api_access": "Teljes API-hozzáférés megszerzése",
"trial_feature_collaboration": "Minden csapat- és együttműködési funkció",
"trial_feature_quotas": "Kvóták kezelése",
"trial_feature_webhooks": "Egyéni webhookok beállítása",
"trial_feature_whitelabel": "Teljesen fehércímkés felmérések",
"trial_feature_api_access": "API-hozzáférés",
"trial_feature_attribute_segmentation": "Attribútumalapú szegmentálás",
"trial_feature_contact_segment_management": "Kapcsolat- és szegmenskezelés",
"trial_feature_email_followups": "E-mail követések",
"trial_feature_hide_branding": "Formbricks márkajelzés elrejtése",
"trial_feature_mobile_sdks": "iOS és Android SDK-k",
"trial_feature_respondent_identification": "Válaszadó-azonosítás",
"trial_feature_unlimited_seats": "Korlátlan számú felhasználói hely",
"trial_feature_webhooks": "Egyéni webhookok",
"trial_no_credit_card": "14 napos próbaidőszak, bankkártya nélkül",
"trial_title": "Próbálja ki a Pro funkciókat ingyen!",
"trial_payment_method_added_description": "Minden rendben! A Pro csomag automatikusan folytatódik a próbaidőszak lejárta után.",
"trial_title": "Szerezze meg a Formbricks Pro-t ingyen!",
"unlimited_responses": "Korlátlan válaszok",
"unlimited_workspaces": "Korlátlan munkaterület",
"upgrade": "Frissítés",

View File

@@ -434,6 +434,9 @@
"title": "タイトル",
"top_left": "左上",
"top_right": "右上",
"trial_days_remaining": "トライアル期間の残り{count}日",
"trial_expired": "トライアル期間が終了しました",
"trial_one_day_remaining": "トライアル期間の残り1日",
"try_again": "もう一度お試しください",
"type": "種類",
"unknown_survey": "不明なフォーム",
@@ -968,6 +971,7 @@
"api_keys_description": "Formbricks管理APIにアクセスするためのAPIキーを管理します"
},
"billing": {
"add_payment_method": "支払い方法を追加",
"cancelling": "キャンセル中",
"failed_to_start_trial": "トライアルの開始に失敗しました。もう一度お試しください。",
"manage_subscription": "サブスクリプションを管理",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "請求情報の設定が正常に完了しませんでした。もう一度やり直してサブスクリプションを有効化してください。",
"subscription": "サブスクリプション",
"subscription_description": "サブスクリプションプランの管理や利用状況の確認はこちら",
"trial_alert_description": "すべての機能へのアクセスを維持するには、支払い方法を追加してください。",
"trial_already_used": "このメールアドレスでは既に無料トライアルが使用されています。代わりに有料プランにアップグレードしてください。",
"trial_feature_api_access": "フルAPIアクセスを利用",
"trial_feature_collaboration": "すべてのチーム・コラボレーション機能",
"trial_feature_quotas": "クォータの管理",
"trial_feature_webhooks": "カスタムWebhookの設定",
"trial_feature_whitelabel": "完全ホワイトラベル対応のアンケート",
"trial_feature_api_access": "APIアクセス",
"trial_feature_attribute_segmentation": "属性ベースのセグメンテーション",
"trial_feature_contact_segment_management": "連絡先とセグメントの管理",
"trial_feature_email_followups": "メールフォローアップ",
"trial_feature_hide_branding": "Formbricksブランディングを非表示",
"trial_feature_mobile_sdks": "iOS & Android SDK",
"trial_feature_respondent_identification": "回答者の識別",
"trial_feature_unlimited_seats": "無制限のシート数",
"trial_feature_webhooks": "カスタムWebhook",
"trial_no_credit_card": "14日間トライアル、クレジットカード不要",
"trial_title": "Pro機能を無料でお試し!",
"trial_payment_method_added_description": "準備完了ですトライアル終了後、Proプランが自動的に継続されます。",
"trial_title": "Formbricks Proを無料で入手しよう!",
"unlimited_responses": "無制限の回答",
"unlimited_workspaces": "無制限ワークスペース",
"upgrade": "アップグレード",

View File

@@ -434,6 +434,9 @@
"title": "Titel",
"top_left": "Linksboven",
"top_right": "Rechtsboven",
"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",
"try_again": "Probeer het opnieuw",
"type": "Type",
"unknown_survey": "Onbekende enquête",
@@ -968,6 +971,7 @@
"api_keys_description": "Beheer API-sleutels om toegang te krijgen tot Formbricks-beheer-API's"
},
"billing": {
"add_payment_method": "Betaalmethode toevoegen",
"cancelling": "Bezig met annuleren",
"failed_to_start_trial": "Proefperiode starten mislukt. Probeer het opnieuw.",
"manage_subscription": "Abonnement beheren",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "Het instellen van de facturatie is niet gelukt. Probeer het opnieuw om je abonnement te activeren.",
"subscription": "Abonnement",
"subscription_description": "Beheer je abonnement en houd je gebruik bij",
"trial_alert_description": "Voeg een betaalmethode toe om toegang te houden tot alle functies.",
"trial_already_used": "Er is al een gratis proefperiode gebruikt voor dit e-mailadres. Upgrade in plaats daarvan naar een betaald abonnement.",
"trial_feature_api_access": "Krijg volledige API-toegang",
"trial_feature_collaboration": "Alle team- en samenwerkingsfuncties",
"trial_feature_quotas": "Quota's beheren",
"trial_feature_webhooks": "Aangepaste webhooks instellen",
"trial_feature_whitelabel": "Volledig white-label enquêtes",
"trial_feature_api_access": "API-toegang",
"trial_feature_attribute_segmentation": "Segmentatie op basis van attributen",
"trial_feature_contact_segment_management": "Contact- en segmentbeheer",
"trial_feature_email_followups": "E-mail follow-ups",
"trial_feature_hide_branding": "Verberg Formbricks-branding",
"trial_feature_mobile_sdks": "iOS- en Android-SDK's",
"trial_feature_respondent_identification": "Identificatie van respondenten",
"trial_feature_unlimited_seats": "Onbeperkt aantal gebruikers",
"trial_feature_webhooks": "Aangepaste webhooks",
"trial_no_credit_card": "14 dagen proefperiode, geen creditcard vereist",
"trial_title": "Probeer Pro-functies gratis!",
"trial_payment_method_added_description": "Je bent helemaal klaar! Je Pro-abonnement wordt automatisch voortgezet na afloop van de proefperiode.",
"trial_title": "Krijg Formbricks Pro gratis!",
"unlimited_responses": "Onbeperkte reacties",
"unlimited_workspaces": "Onbeperkt werkruimtes",
"upgrade": "Upgraden",

View File

@@ -434,6 +434,9 @@
"title": "Título",
"top_left": "Canto superior esquerdo",
"top_right": "Canto Superior Direito",
"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",
"try_again": "Tenta de novo",
"type": "Tipo",
"unknown_survey": "Pesquisa desconhecida",
@@ -968,6 +971,7 @@
"api_keys_description": "Gerencie chaves de API para acessar as APIs de gerenciamento do Formbricks"
},
"billing": {
"add_payment_method": "Adicionar forma de pagamento",
"cancelling": "Cancelando",
"failed_to_start_trial": "Falha ao iniciar o período de teste. Por favor, tente novamente.",
"manage_subscription": "Gerenciar assinatura",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "A configuração de cobrança não foi concluída com sucesso. Tente novamente para ativar sua assinatura.",
"subscription": "Assinatura",
"subscription_description": "Gerencie seu plano de assinatura e acompanhe seu uso",
"trial_alert_description": "Adicione uma forma de pagamento para manter o acesso a todos os recursos.",
"trial_already_used": "Um período de teste gratuito já foi usado para este endereço de e-mail. Por favor, faça upgrade para um plano pago.",
"trial_feature_api_access": "Obtenha acesso completo à API",
"trial_feature_collaboration": "Todos os recursos de equipe e colaboração",
"trial_feature_quotas": "Gerencie cotas",
"trial_feature_webhooks": "Configure webhooks personalizados",
"trial_feature_whitelabel": "Pesquisas totalmente personalizadas",
"trial_feature_api_access": "Acesso à API",
"trial_feature_attribute_segmentation": "Segmentação Baseada em Atributos",
"trial_feature_contact_segment_management": "Gerenciamento de Contatos e Segmentos",
"trial_feature_email_followups": "Follow-ups por E-mail",
"trial_feature_hide_branding": "Ocultar Marca Formbricks",
"trial_feature_mobile_sdks": "SDKs para iOS e Android",
"trial_feature_respondent_identification": "Identificação de Respondentes",
"trial_feature_unlimited_seats": "Assentos Ilimitados",
"trial_feature_webhooks": "Webhooks Personalizados",
"trial_no_credit_card": "14 dias de teste, sem necessidade de cartão de crédito",
"trial_title": "Experimente os recursos Pro gratuitamente!",
"trial_payment_method_added_description": "Tudo pronto! Seu plano Pro continuará automaticamente após o término do período de teste.",
"trial_title": "Ganhe o Formbricks Pro gratuitamente!",
"unlimited_responses": "Respostas Ilimitadas",
"unlimited_workspaces": "Projetos ilimitados",
"upgrade": "Atualizar",

View File

@@ -434,6 +434,9 @@
"title": "Título",
"top_left": "Superior Esquerdo",
"top_right": "Superior Direito",
"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",
"try_again": "Tente novamente",
"type": "Tipo",
"unknown_survey": "Inquérito desconhecido",
@@ -968,6 +971,7 @@
"api_keys_description": "Faça a gestão das suas chaves API para aceder às APIs de gestão do Formbricks"
},
"billing": {
"add_payment_method": "Adicionar método de pagamento",
"cancelling": "A cancelar",
"failed_to_start_trial": "Falha ao iniciar o período de teste. Por favor, tenta novamente.",
"manage_subscription": "Gerir subscrição",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "A configuração de faturação não foi concluída com sucesso. Por favor, tenta novamente para ativar a tua subscrição.",
"subscription": "Subscrição",
"subscription_description": "Gere o teu plano de subscrição e acompanha a tua utilização",
"trial_alert_description": "Adiciona um método de pagamento para manteres acesso a todas as funcionalidades.",
"trial_already_used": "Já foi utilizado um período de teste gratuito para este endereço de email. Por favor, atualiza para um plano pago.",
"trial_feature_api_access": "Obtém acesso completo à API",
"trial_feature_collaboration": "Todas as funcionalidades de equipa e colaboração",
"trial_feature_quotas": "Gere quotas",
"trial_feature_webhooks": "Configura webhooks personalizados",
"trial_feature_whitelabel": "Inquéritos totalmente personalizados",
"trial_feature_api_access": "Acesso à API",
"trial_feature_attribute_segmentation": "Segmentação Baseada em Atributos",
"trial_feature_contact_segment_management": "Gestão de Contactos e Segmentos",
"trial_feature_email_followups": "Seguimentos por E-mail",
"trial_feature_hide_branding": "Ocultar Marca Formbricks",
"trial_feature_mobile_sdks": "SDKs para iOS e Android",
"trial_feature_respondent_identification": "Identificação de Inquiridos",
"trial_feature_unlimited_seats": "Lugares Ilimitados",
"trial_feature_webhooks": "Webhooks Personalizados",
"trial_no_credit_card": "14 dias de teste, sem necessidade de cartão de crédito",
"trial_title": "Experimenta as funcionalidades Pro gratuitamente!",
"trial_payment_method_added_description": "Está tudo pronto! O teu plano Pro continuará automaticamente após o fim do período experimental.",
"trial_title": "Obtém o Formbricks Pro gratuitamente!",
"unlimited_responses": "Respostas Ilimitadas",
"unlimited_workspaces": "Projetos ilimitados",
"upgrade": "Atualizar",

View File

@@ -434,6 +434,9 @@
"title": "Titlu",
"top_left": "Stânga Sus",
"top_right": "Dreapta Sus",
"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ă",
"try_again": "Încearcă din nou",
"type": "Tip",
"unknown_survey": "Chestionar necunoscut",
@@ -968,6 +971,7 @@
"api_keys_description": "Gestionați cheile API pentru a accesa API-urile de administrare Formbricks"
},
"billing": {
"add_payment_method": "Adaugă o metodă de plată",
"cancelling": "Anulare în curs",
"failed_to_start_trial": "Nu am putut porni perioada de probă. Te rugăm să încerci din nou.",
"manage_subscription": "Gestionează abonamentul",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "Configurarea facturării nu a fost finalizată cu succes. Încearcă din nou pentru a activa abonamentul.",
"subscription": "Abonament",
"subscription_description": "Gestionează-ți abonamentul și monitorizează-ți consumul",
"trial_alert_description": "Adaugă o metodă de plată pentru a păstra accesul la toate funcționalitățile.",
"trial_already_used": "O perioadă de probă gratuită a fost deja utilizată pentru această adresă de email. Te rugăm să treci la un plan plătit în schimb.",
"trial_feature_api_access": "Obține acces complet la API",
"trial_feature_collaboration": "Toate funcțiile de echipă și colaborare",
"trial_feature_quotas": "Gestionează cotele",
"trial_feature_webhooks": "Configurează webhook-uri personalizate",
"trial_feature_whitelabel": "Chestionare complet personalizate (white-label)",
"trial_feature_api_access": "Acces API",
"trial_feature_attribute_segmentation": "Segmentare bazată pe atribute",
"trial_feature_contact_segment_management": "Gestionare contacte și segmente",
"trial_feature_email_followups": "Urmăriri prin email",
"trial_feature_hide_branding": "Ascunde branding-ul Formbricks",
"trial_feature_mobile_sdks": "SDK-uri iOS și Android",
"trial_feature_respondent_identification": "Identificarea respondenților",
"trial_feature_unlimited_seats": "Locuri nelimitate",
"trial_feature_webhooks": "Webhook-uri personalizate",
"trial_no_credit_card": "14 zile de probă, fără card necesar",
"trial_title": "Încearcă funcțiile Pro gratuit!",
"trial_payment_method_added_description": "Totul este pregătit! Planul tău Pro va continua automat după ce se încheie perioada de probă.",
"trial_title": "Obține Formbricks Pro gratuit!",
"unlimited_responses": "Răspunsuri nelimitate",
"unlimited_workspaces": "Workspaces nelimitate",
"upgrade": "Actualizare",

View File

@@ -434,6 +434,9 @@
"title": "Заголовок",
"top_left": "Вверху слева",
"top_right": "Вверху справа",
"trial_days_remaining": "{count, plural, one {Остался # день пробного периода} few {Осталось # дня пробного периода} many {Осталось # дней пробного периода} other {Осталось # дней пробного периода}}",
"trial_expired": "Пробный период истёк",
"trial_one_day_remaining": "Остался 1 день пробного периода",
"try_again": "Попробуйте ещё раз",
"type": "Тип",
"unknown_survey": "Неизвестный опрос",
@@ -968,6 +971,7 @@
"api_keys_description": "Управляйте API-ключами для доступа к управляющим API Formbricks"
},
"billing": {
"add_payment_method": "Добавить способ оплаты",
"cancelling": "Отмена",
"failed_to_start_trial": "Не удалось запустить пробный период. Попробуйте снова.",
"manage_subscription": "Управление подпиской",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "Настройка оплаты не была завершена. Пожалуйста, повторите попытку, чтобы активировать вашу подписку.",
"subscription": "Подписка",
"subscription_description": "Управляйте своим тарифом и следите за использованием",
"trial_alert_description": "Добавьте способ оплаты, чтобы сохранить доступ ко всем функциям.",
"trial_already_used": "Бесплатный пробный период уже был использован для этого адреса электронной почты. Пожалуйста, перейдите на платный тариф.",
"trial_feature_api_access": "Получите полный доступ к API",
"trial_feature_collaboration": "Все функции для работы в команде и совместной работы",
"trial_feature_quotas": "Управляйте квотами",
"trial_feature_webhooks": "Настройте собственные вебхуки",
"trial_feature_whitelabel": "Полностью персонализированные опросы без брендинга",
"trial_feature_api_access": "Доступ к API",
"trial_feature_attribute_segmentation": "Сегментация на основе атрибутов",
"trial_feature_contact_segment_management": "Управление контактами и сегментами",
"trial_feature_email_followups": "Email-уведомления",
"trial_feature_hide_branding": "Скрыть брендинг Formbricks",
"trial_feature_mobile_sdks": "iOS и Android SDK",
"trial_feature_respondent_identification": "Идентификация респондентов",
"trial_feature_unlimited_seats": "Неограниченное количество мест",
"trial_feature_webhooks": "Пользовательские вебхуки",
"trial_no_credit_card": "14 дней пробного периода, кредитная карта не требуется",
"trial_title": "Попробуйте Pro функции бесплатно!",
"trial_payment_method_added_description": "Всё готово! Твой тарифный план Pro продолжится автоматически после окончания пробного периода.",
"trial_title": "Получите Formbricks Pro бесплатно!",
"unlimited_responses": "Неограниченное количество ответов",
"unlimited_workspaces": "Неограниченное количество рабочих пространств",
"upgrade": "Обновить",

View File

@@ -434,6 +434,9 @@
"title": "Titel",
"top_left": "Övre vänster",
"top_right": "Övre höger",
"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",
"try_again": "Försök igen",
"type": "Typ",
"unknown_survey": "Okänd enkät",
@@ -968,6 +971,7 @@
"api_keys_description": "Hantera API-nycklar för åtkomst till Formbricks hanterings-API:er"
},
"billing": {
"add_payment_method": "Lägg till betalningsmetod",
"cancelling": "Avbryter",
"failed_to_start_trial": "Kunde inte starta provperioden. Försök igen.",
"manage_subscription": "Hantera prenumeration",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "Faktureringsinställningen slutfördes inte riktigt. Försök igen för att aktivera ditt abonnemang.",
"subscription": "Abonnemang",
"subscription_description": "Hantera din abonnemangsplan och följ din användning",
"trial_alert_description": "Lägg till en betalningsmetod för att behålla tillgång till alla funktioner.",
"trial_already_used": "En gratis provperiod har redan använts för denna e-postadress. Uppgradera till en betald plan istället.",
"trial_feature_api_access": "Få full API-åtkomst",
"trial_feature_collaboration": "Alla team- och samarbetsfunktioner",
"trial_feature_quotas": "Hantera kvoter",
"trial_feature_webhooks": "Konfigurera anpassade webhooks",
"trial_feature_whitelabel": "Helt white-label-anpassade enkäter",
"trial_feature_api_access": "API-åtkomst",
"trial_feature_attribute_segmentation": "Attributbaserad segmentering",
"trial_feature_contact_segment_management": "Kontakt- och segmenthantering",
"trial_feature_email_followups": "E-postuppföljningar",
"trial_feature_hide_branding": "Dölj Formbricks-branding",
"trial_feature_mobile_sdks": "iOS- och Android-SDK:er",
"trial_feature_respondent_identification": "Respondentidentifiering",
"trial_feature_unlimited_seats": "Obegränsade platser",
"trial_feature_webhooks": "Anpassade webhooks",
"trial_no_credit_card": "14 dagars provperiod, inget kreditkort krävs",
"trial_title": "Testa Pro-funktioner gratis!",
"trial_payment_method_added_description": "Du är redo! Din Pro-plan kommer att fortsätta automatiskt efter att provperioden slutar.",
"trial_title": "Få Formbricks Pro gratis!",
"unlimited_responses": "Obegränsade svar",
"unlimited_workspaces": "Obegränsat antal arbetsytor",
"upgrade": "Uppgradera",

View File

@@ -434,6 +434,9 @@
"title": "标题",
"top_left": "左上",
"top_right": "右上",
"trial_days_remaining": "试用期还剩 {count} 天",
"trial_expired": "您的试用期已过期",
"trial_one_day_remaining": "试用期还剩 1 天",
"try_again": "再试一次",
"type": "类型",
"unknown_survey": "未知调查",
@@ -968,6 +971,7 @@
"api_keys_description": "管理 API 密钥 以 访问 Formbricks 管理 API"
},
"billing": {
"add_payment_method": "添加支付方式",
"cancelling": "正在取消",
"failed_to_start_trial": "试用启动失败,请重试。",
"manage_subscription": "管理订阅",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "账单设置未成功完成。请重试以激活订阅。",
"subscription": "订阅",
"subscription_description": "管理你的订阅套餐并监控用量",
"trial_alert_description": "添加支付方式以继续使用所有功能。",
"trial_already_used": "该邮箱地址已使用过免费试用。请升级至付费计划。",
"trial_feature_api_access": "获取完整 API 访问权限",
"trial_feature_collaboration": "所有团队和协作功能",
"trial_feature_quotas": "管理配额",
"trial_feature_webhooks": "设置自定义 Webhook",
"trial_feature_whitelabel": "完全白标化的问卷调查",
"trial_feature_api_access": "API 访问",
"trial_feature_attribute_segmentation": "基于属性的细分",
"trial_feature_contact_segment_management": "联系人和细分管理",
"trial_feature_email_followups": "电子邮件跟进",
"trial_feature_hide_branding": "隐藏 Formbricks 品牌标识",
"trial_feature_mobile_sdks": "iOS 和 Android SDK",
"trial_feature_respondent_identification": "受访者识别",
"trial_feature_unlimited_seats": "无限席位",
"trial_feature_webhooks": "自定义 Webhook",
"trial_no_credit_card": "14 天试用,无需信用卡",
"trial_title": "免费试用专业版功能!",
"trial_payment_method_added_description": "一切就绪!试用期结束后,您的专业版计划将自动继续。",
"trial_title": "免费获取 Formbricks Pro!",
"unlimited_responses": "无限反馈",
"unlimited_workspaces": "无限工作区",
"upgrade": "升级",

View File

@@ -434,6 +434,9 @@
"title": "標題",
"top_left": "左上",
"top_right": "右上",
"trial_days_remaining": "試用期剩餘 {count} 天",
"trial_expired": "您的試用期已結束",
"trial_one_day_remaining": "試用期剩餘 1 天",
"try_again": "再試一次",
"type": "類型",
"unknown_survey": "未知問卷",
@@ -968,6 +971,7 @@
"api_keys_description": "管理 API 金鑰以存取 Formbricks 管理 API"
},
"billing": {
"add_payment_method": "新增付款方式",
"cancelling": "正在取消",
"failed_to_start_trial": "無法開始試用。請再試一次。",
"manage_subscription": "管理訂閱",
@@ -992,14 +996,20 @@
"stripe_setup_incomplete_description": "帳單設定未成功完成,請重新操作以啟用訂閱。",
"subscription": "訂閱",
"subscription_description": "管理您的訂閱方案並監控用量",
"trial_alert_description": "新增付款方式以繼續使用所有功能。",
"trial_already_used": "此電子郵件地址已使用過免費試用。請改為升級至付費方案。",
"trial_feature_api_access": "獲得完整 API 存取權限",
"trial_feature_collaboration": "所有團隊與協作功能",
"trial_feature_quotas": "管理配額",
"trial_feature_webhooks": "設定自訂 Webhook",
"trial_feature_whitelabel": "完全白標問卷調查",
"trial_feature_api_access": "API 存取",
"trial_feature_attribute_segmentation": "基於屬性的分群",
"trial_feature_contact_segment_management": "聯絡人與分群管理",
"trial_feature_email_followups": "電子郵件追蹤",
"trial_feature_hide_branding": "隱藏 Formbricks 品牌標識",
"trial_feature_mobile_sdks": "iOS 與 Android SDK",
"trial_feature_respondent_identification": "受訪者識別",
"trial_feature_unlimited_seats": "無限座位數",
"trial_feature_webhooks": "自訂 Webhook",
"trial_no_credit_card": "14 天試用,無需信用卡",
"trial_title": "免費試用 Pro 功能!",
"trial_payment_method_added_description": "一切就緒!試用期結束後,您的 Pro 方案將自動繼續。",
"trial_title": "免費獲得 Formbricks Pro",
"unlimited_responses": "無限回應",
"unlimited_workspaces": "無限工作區",
"upgrade": "升級",

View File

@@ -10,9 +10,10 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-clie
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { createCustomerPortalSession } from "@/modules/ee/billing/api/lib/create-customer-portal-session";
import { createSetupCheckoutSession } from "@/modules/ee/billing/api/lib/create-setup-checkout-session";
import { isSubscriptionCancelled } from "@/modules/ee/billing/api/lib/is-subscription-cancelled";
import {
createScaleTrialSubscription,
createProTrialSubscription,
ensureCloudStripeSetupForOrganization,
reconcileCloudStripeSubscriptionsForOrganization,
syncOrganizationBillingFromStripe,
@@ -145,11 +146,59 @@ export const retryStripeSetupAction = authenticatedActionClient
return { success: true };
});
const ZCreateTrialPaymentCheckoutAction = z.object({
environmentId: ZId,
});
export const createTrialPaymentCheckoutAction = authenticatedActionClient
.inputSchema(ZCreateTrialPaymentCheckoutAction)
.action(
withAuditLogging("subscriptionAccessed", "organization", async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager", "billing"],
},
],
});
const organization = await getOrganization(organizationId);
if (!organization) {
throw new ResourceNotFoundError("organization", organizationId);
}
if (!organization.billing.stripeCustomerId) {
throw new AuthorizationError("You do not have an associated Stripe CustomerId");
}
const subscriptionId = organization.billing.stripe?.subscriptionId;
if (!subscriptionId) {
throw new ResourceNotFoundError("subscription", organizationId);
}
ctx.auditLoggingCtx.organizationId = organizationId;
const returnUrl = `${WEBAPP_URL}/environments/${parsedInput.environmentId}/settings/billing`;
const checkoutUrl = await createSetupCheckoutSession(
organization.billing.stripeCustomerId,
subscriptionId,
returnUrl,
organizationId
);
ctx.auditLoggingCtx.newObject = { checkoutUrl };
return checkoutUrl;
})
);
const ZStartScaleTrialAction = z.object({
organizationId: ZId,
});
export const startScaleTrialAction = authenticatedActionClient
export const startProTrialAction = authenticatedActionClient
.inputSchema(ZStartScaleTrialAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
@@ -172,8 +221,8 @@ export const startScaleTrialAction = authenticatedActionClient
throw new ResourceNotFoundError("OrganizationBilling", parsedInput.organizationId);
}
await createScaleTrialSubscription(parsedInput.organizationId, organization.billing.stripeCustomerId);
await reconcileCloudStripeSubscriptionsForOrganization(parsedInput.organizationId, "scale-trial");
await createProTrialSubscription(parsedInput.organizationId, organization.billing.stripeCustomerId);
await reconcileCloudStripeSubscriptionsForOrganization(parsedInput.organizationId, "pro-trial");
await syncOrganizationBillingFromStripe(parsedInput.organizationId);
return { success: true };
});

View File

@@ -0,0 +1,52 @@
import Stripe from "stripe";
import { STRIPE_API_VERSION } from "@/lib/constants";
import { env } from "@/lib/env";
/**
* Creates a Stripe Checkout Session in `setup` mode so the customer can enter
* a payment method, billing address, and tax ID — without creating a new subscription.
* After completion the webhook handler attaches the payment method to the existing
* trial subscription.
*/
export const createSetupCheckoutSession = async (
stripeCustomerId: string,
subscriptionId: string,
returnUrl: string,
organizationId: string
): Promise<string> => {
if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
apiVersion: STRIPE_API_VERSION as Stripe.LatestApiVersion,
});
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const currency = subscription.currency ?? "usd";
const session = await stripe.checkout.sessions.create({
mode: "setup",
customer: stripeCustomerId,
currency,
billing_address_collection: "required",
tax_id_collection: {
enabled: true,
required: "if_supported",
},
customer_update: {
address: "auto",
name: "auto",
},
success_url: `${returnUrl}?checkout_success=1`,
cancel_url: returnUrl,
metadata: {
organizationId,
subscriptionId,
},
});
if (!session.url) {
throw new Error("Stripe did not return a Checkout Session URL");
}
return session.url;
};

View File

@@ -16,6 +16,47 @@ const relevantEvents = new Set([
"entitlements.active_entitlement_summary.updated",
]);
/**
* When a setup-mode Checkout Session completes, the customer has just provided a
* payment method + billing address. We attach that payment method as the default
* on the customer (for future invoices) and on the trial subscription so Stripe
* can charge it when the trial ends.
*/
const handleSetupCheckoutCompleted = async (
session: Stripe.Checkout.Session,
stripe: Stripe
): Promise<void> => {
if (session.mode !== "setup" || !session.setup_intent) return;
const setupIntentId =
typeof session.setup_intent === "string" ? session.setup_intent : session.setup_intent.id;
const setupIntent = await stripe.setupIntents.retrieve(setupIntentId);
const paymentMethodId =
typeof setupIntent.payment_method === "string"
? setupIntent.payment_method
: setupIntent.payment_method?.id;
if (!paymentMethodId) {
logger.warn({ sessionId: session.id }, "Setup checkout completed but no payment method found");
return;
}
const customerId = typeof session.customer === "string" ? session.customer : session.customer?.id;
if (customerId) {
await stripe.customers.update(customerId, {
invoice_settings: { default_payment_method: paymentMethodId },
});
}
const subscriptionId = session.metadata?.subscriptionId;
if (subscriptionId) {
await stripe.subscriptions.update(subscriptionId, {
default_payment_method: paymentMethodId,
});
}
};
const getMetadataOrganizationId = (eventObject: Stripe.Event.Data.Object): string | null => {
if (!("metadata" in eventObject) || !eventObject.metadata) {
return null;
@@ -101,6 +142,10 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin
}
try {
if (event.type === "checkout.session.completed") {
await handleSetupCheckoutCompleted(event.data.object, stripe);
}
await reconcileCloudStripeSubscriptionsForOrganization(organizationId, event.id);
await syncOrganizationBillingFromStripe(organizationId, {
id: event.id,

View File

@@ -1,6 +1,6 @@
"use client";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import Script from "next/script";
import { createElement, useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
@@ -12,10 +12,12 @@ import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import {
createPricingTableCustomerSessionAction,
createTrialPaymentCheckoutAction,
isSubscriptionCancelledAction,
manageSubscriptionAction,
retryStripeSetupAction,
} from "../actions";
import { TrialAlert } from "./trial-alert";
import { UsageCard } from "./usage-card";
const STRIPE_SUPPORTED_LOCALES = new Set([
@@ -92,6 +94,7 @@ interface PricingTableProps {
stripePublishableKey: string | null;
stripePricingTableId: string | null;
isStripeSetupIncomplete: boolean;
trialDaysRemaining: number | null;
}
const getCurrentCloudPlanLabel = (
@@ -118,9 +121,11 @@ export const PricingTable = ({
stripePublishableKey,
stripePricingTableId,
isStripeSetupIncomplete,
trialDaysRemaining,
}: PricingTableProps) => {
const { t, i18n } = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
const [isRetryingStripeSetup, setIsRetryingStripeSetup] = useState(false);
const [cancellingOn, setCancellingOn] = useState<Date | null>(null);
const [pricingTableCustomerSessionClientSecret, setPricingTableCustomerSessionClientSecret] = useState<
@@ -128,8 +133,9 @@ export const PricingTable = ({
>(null);
const isUpgradeablePlan = currentCloudPlan === "hobby" || currentCloudPlan === "unknown";
const isTrialing = currentSubscriptionStatus === "trialing";
const showPricingTable =
hasBillingRights && isUpgradeablePlan && !!stripePublishableKey && !!stripePricingTableId;
hasBillingRights && isUpgradeablePlan && !isTrialing && !!stripePublishableKey && !!stripePricingTableId;
const canManageSubscription =
hasBillingRights && !isUpgradeablePlan && !!organization.billing.stripeCustomerId;
const stripeLocaleOverride = useMemo(
@@ -161,6 +167,13 @@ export const PricingTable = ({
stripePublishableKey,
]);
useEffect(() => {
if (searchParams.get("checkout_success")) {
const timer = setTimeout(() => router.refresh(), 2500);
return () => clearTimeout(timer);
}
}, [searchParams, router]);
useEffect(() => {
const checkSubscriptionStatus = async () => {
if (!hasBillingRights || !canManageSubscription) {
@@ -213,6 +226,20 @@ export const PricingTable = ({
}
};
const openTrialPaymentCheckout = async () => {
try {
const response = await createTrialPaymentCheckoutAction({ environmentId });
if (response?.data && typeof response.data === "string") {
globalThis.location.href = response.data;
} else {
toast.error(t("common.something_went_wrong_please_try_again"));
}
} catch (error) {
console.error("Failed to create checkout session:", error);
toast.error(t("common.something_went_wrong_please_try_again"));
}
};
const retryStripeSetup = async () => {
setIsRetryingStripeSetup(true);
try {
@@ -246,6 +273,25 @@ export const PricingTable = ({
return (
<main>
<div className="flex max-w-4xl flex-col gap-4">
{trialDaysRemaining !== null &&
(organization.billing.stripe?.hasPaymentMethod ? (
<TrialAlert trialDaysRemaining={trialDaysRemaining} hasPaymentMethod>
<AlertDescription>
{t("environments.settings.billing.trial_payment_method_added_description")}
</AlertDescription>
</TrialAlert>
) : (
<TrialAlert trialDaysRemaining={trialDaysRemaining}>
<AlertDescription>
{t("environments.settings.billing.trial_alert_description")}
</AlertDescription>
{hasBillingRights && (
<AlertButton onClick={() => void openTrialPaymentCheckout()}>
{t("environments.settings.billing.add_payment_method")}
</AlertButton>
)}
</TrialAlert>
))}
{isStripeSetupIncomplete && hasBillingRights && (
<Alert variant="warning">
<AlertTitle>{t("environments.settings.billing.stripe_setup_incomplete")}</AlertTitle>
@@ -261,7 +307,7 @@ export const PricingTable = ({
title={t("environments.settings.billing.subscription")}
description={t("environments.settings.billing.subscription_description")}
buttonInfo={
canManageSubscription
canManageSubscription && currentSubscriptionStatus !== "trialing"
? {
text: t("environments.settings.billing.manage_subscription"),
onClick: () => void openCustomerPortal(),
@@ -324,7 +370,7 @@ export const PricingTable = ({
</div>
</SettingsCard>
{currentCloudPlan === "pro" && (
{currentCloudPlan === "pro" && !isTrialing && (
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-slate-800 p-6 shadow-sm">
<div className="flex items-center justify-between gap-6">
<div className="flex flex-col gap-1.5">

View File

@@ -11,7 +11,7 @@ import ethereumLogo from "@/images/customer-logos/ethereum-logo.png";
import flixbusLogo from "@/images/customer-logos/flixbus-white.svg";
import githubLogo from "@/images/customer-logos/github-logo.png";
import siemensLogo from "@/images/customer-logos/siemens.png";
import { startScaleTrialAction } from "@/modules/ee/billing/actions";
import { startProTrialAction } from "@/modules/ee/billing/actions";
import { Button } from "@/modules/ui/components/button";
interface SelectPlanCardProps {
@@ -34,17 +34,21 @@ export const SelectPlanCard = ({ nextUrl, organizationId }: SelectPlanCardProps)
const { t } = useTranslation();
const TRIAL_FEATURE_KEYS = [
t("environments.settings.billing.trial_feature_whitelabel"),
t("environments.settings.billing.trial_feature_collaboration"),
t("environments.settings.billing.trial_feature_unlimited_seats"),
t("environments.settings.billing.trial_feature_hide_branding"),
t("environments.settings.billing.trial_feature_respondent_identification"),
t("environments.settings.billing.trial_feature_contact_segment_management"),
t("environments.settings.billing.trial_feature_attribute_segmentation"),
t("environments.settings.billing.trial_feature_mobile_sdks"),
t("environments.settings.billing.trial_feature_email_followups"),
t("environments.settings.billing.trial_feature_webhooks"),
t("environments.settings.billing.trial_feature_api_access"),
t("environments.settings.billing.trial_feature_quotas"),
] as const;
const handleStartTrial = async () => {
setIsStartingTrial(true);
try {
const result = await startScaleTrialAction({ organizationId });
const result = await startProTrialAction({ organizationId });
if (result?.data) {
router.push(nextUrl);
} else if (result?.serverError === "trial_already_used") {

View File

@@ -0,0 +1,44 @@
"use client";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Alert, AlertTitle } from "@/modules/ui/components/alert";
type TrialAlertVariant = "error" | "warning" | "info" | "success";
const getTrialVariant = (daysRemaining: number): TrialAlertVariant => {
if (daysRemaining <= 3) return "error";
if (daysRemaining <= 7) return "warning";
return "info";
};
interface TrialAlertProps {
trialDaysRemaining: number;
size?: "small";
hasPaymentMethod?: boolean;
children?: React.ReactNode;
}
export const TrialAlert = ({
trialDaysRemaining,
size,
hasPaymentMethod = false,
children,
}: TrialAlertProps) => {
const { t } = useTranslation();
const title = useMemo(() => {
if (trialDaysRemaining <= 0) return t("common.trial_expired");
if (trialDaysRemaining === 1) return t("common.trial_one_day_remaining");
return t("common.trial_days_remaining", { count: trialDaysRemaining });
}, [trialDaysRemaining, t]);
const variant = hasPaymentMethod ? "success" : getTrialVariant(trialDaysRemaining);
return (
<Alert variant={variant} size={size}>
<AlertTitle>{title}</AlertTitle>
{children}
</Alert>
);
};

View File

@@ -30,6 +30,7 @@ describe("cloud-billing-display", () => {
organizationId: "org_1",
currentCloudPlan: "pro",
currentSubscriptionStatus: null,
trialDaysRemaining: null,
usageCycleStart: new Date("2026-01-15T00:00:00.000Z"),
usageCycleEnd: new Date("2026-02-15T00:00:00.000Z"),
billing,

View File

@@ -10,6 +10,7 @@ export type TCloudBillingDisplayContext = {
organizationId: string;
currentCloudPlan: TCloudBillingDisplayPlan;
currentSubscriptionStatus: TOrganizationStripeSubscriptionStatus | null;
trialDaysRemaining: number | null;
usageCycleStart: Date;
usageCycleEnd: Date;
billing: NonNullable<Awaited<ReturnType<typeof getOrganizationBillingWithReadThroughSync>>>;
@@ -27,6 +28,22 @@ const resolveCurrentSubscriptionStatus = (
return billing.stripe?.subscriptionStatus ?? null;
};
const MS_PER_DAY = 86_400_000;
const resolveTrialDaysRemaining = (
billing: NonNullable<Awaited<ReturnType<typeof getOrganizationBillingWithReadThroughSync>>>
): number | null => {
if (billing.stripe?.subscriptionStatus !== "trialing" || !billing.stripe.trialEnd) {
return null;
}
const trialEndDate = new Date(billing.stripe.trialEnd);
if (!Number.isFinite(trialEndDate.getTime())) {
return null;
}
return Math.ceil((trialEndDate.getTime() - Date.now()) / MS_PER_DAY);
};
export const getCloudBillingDisplayContext = async (
organizationId: string
): Promise<TCloudBillingDisplayContext> => {
@@ -42,6 +59,7 @@ export const getCloudBillingDisplayContext = async (
organizationId,
currentCloudPlan: resolveCurrentCloudPlan(billing),
currentSubscriptionStatus: resolveCurrentSubscriptionStatus(billing),
trialDaysRemaining: resolveTrialDaysRemaining(billing),
usageCycleStart: usageCycleWindow.start,
usageCycleEnd: usageCycleWindow.end,
billing,

View File

@@ -300,10 +300,10 @@ const ensureHobbySubscription = async (
};
/**
* Checks whether the given email has already used a Scale trial on any Stripe customer.
* Checks whether the given email has already used a Pro trial on any Stripe customer.
* Searches all customers with that email and inspects their subscription history.
*/
const hasEmailUsedScaleTrial = async (email: string, scaleProductId: string): Promise<boolean> => {
const hasEmailUsedProTrial = async (email: string, proProductId: string): Promise<boolean> => {
if (!stripeClient) return false;
const customers = await stripeClient.customers.list({
@@ -318,23 +318,23 @@ const hasEmailUsedScaleTrial = async (email: string, scaleProductId: string): Pr
limit: 100,
});
const hadScaleTrial = subscriptions.data.some(
const hadProTrial = subscriptions.data.some(
(sub) =>
sub.trial_start != null &&
sub.items.data.some((item) => {
const productId =
typeof item.price.product === "string" ? item.price.product : item.price.product.id;
return productId === scaleProductId;
return productId === proProductId;
})
);
if (hadScaleTrial) return true;
if (hadProTrial) return true;
}
return false;
};
export const createScaleTrialSubscription = async (
export const createProTrialSubscription = async (
organizationId: string,
customerId: string
): Promise<void> => {
@@ -345,30 +345,29 @@ export const createScaleTrialSubscription = async (
limit: 100,
});
const scaleProduct = products.data.find((product) => product.metadata.formbricks_plan === "scale");
if (!scaleProduct) {
throw new Error("Stripe product metadata formbricks_plan=scale not found");
const proProduct = products.data.find((product) => product.metadata.formbricks_plan === "pro");
if (!proProduct) {
throw new Error("Stripe product metadata formbricks_plan=pro not found");
}
// Check if the email has already used a Scale trial across any Stripe customer
const customer = await stripeClient.customers.retrieve(customerId);
if (!customer.deleted && customer.email) {
const alreadyUsed = await hasEmailUsedScaleTrial(customer.email, scaleProduct.id);
const alreadyUsed = await hasEmailUsedProTrial(customer.email, proProduct.id);
if (alreadyUsed) {
throw new OperationNotAllowedError("trial_already_used");
}
}
const defaultPrice =
typeof scaleProduct.default_price === "string" ? null : (scaleProduct.default_price ?? null);
typeof proProduct.default_price === "string" ? null : (proProduct.default_price ?? null);
const fallbackPrices = await stripeClient.prices.list({
product: scaleProduct.id,
product: proProduct.id,
active: true,
limit: 100,
});
const scalePrice =
const proPrice =
defaultPrice ??
fallbackPrices.data.find(
(price) => price.recurring?.interval === "month" && price.recurring.usage_type === "licensed"
@@ -376,14 +375,14 @@ export const createScaleTrialSubscription = async (
fallbackPrices.data[0] ??
null;
if (!scalePrice) {
throw new Error(`No active price found for Stripe scale product ${scaleProduct.id}`);
if (!proPrice) {
throw new Error(`No active price found for Stripe pro product ${proProduct.id}`);
}
await stripeClient.subscriptions.create(
{
customer: customerId,
items: [{ price: scalePrice.id, quantity: 1 }],
items: [{ price: proPrice.id, quantity: 1 }],
trial_period_days: 14,
trial_settings: {
end_behavior: {
@@ -395,7 +394,7 @@ export const createScaleTrialSubscription = async (
},
metadata: { organizationId },
},
{ idempotencyKey: `create-scale-trial-${organizationId}` }
{ idempotencyKey: `create-pro-trial-${organizationId}` }
);
};
@@ -629,10 +628,14 @@ export const syncOrganizationBillingFromStripe = async (
plan: cloudPlan,
subscriptionStatus,
subscriptionId: subscription?.id ?? null,
hasPaymentMethod: subscription?.default_payment_method != null,
features: featureLookupKeys,
lastStripeEventCreatedAt: toIsoStringOrNull(incomingEventDate ?? previousEventDate),
lastSyncedAt: new Date().toISOString(),
lastSyncedEventId: event?.id ?? existingStripeSnapshot?.lastSyncedEventId ?? null,
trialEnd: subscription?.trial_end
? new Date(subscription.trial_end * 1000).toISOString()
: (existingStripeSnapshot?.trialEnd ?? null),
},
};

View File

@@ -59,6 +59,7 @@ export const PricingPage = async (props: { params: Promise<{ environmentId: stri
stripePublishableKey={env.STRIPE_PUBLISHABLE_KEY ?? null}
stripePricingTableId={env.STRIPE_PRICING_TABLE_ID ?? null}
isStripeSetupIncomplete={!organizationWithSyncedBilling.billing.stripeCustomerId}
trialDaysRemaining={cloudBillingDisplayContext.trialDaysRemaining}
/>
</PageContentWrapper>
);

View File

@@ -19,10 +19,12 @@ export const ZOrganizationStripeBilling = z.object({
plan: ZCloudBillingPlan.optional(),
subscriptionStatus: ZOrganizationStripeSubscriptionStatus.nullable().optional(),
subscriptionId: z.string().nullable().optional(),
hasPaymentMethod: z.boolean().optional(),
features: z.array(z.string()).optional(),
lastStripeEventCreatedAt: z.string().nullable().optional(),
lastSyncedAt: z.string().nullable().optional(),
lastSyncedEventId: z.string().nullable().optional(),
trialEnd: z.string().nullable().optional(),
});
export type TOrganizationStripeBilling = z.infer<typeof ZOrganizationStripeBilling>;