diff --git a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx index 10435cdfdf..6d05446270 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx @@ -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 = ({ )} + {/* Trial Days Remaining */} + {!isCollapsed && isFormbricksCloud && trialDaysRemaining !== null && ( + + + + )} + {/* User Switch */}
diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index 8a4b00857d..c4fa7197cd 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -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 diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 7cd9ad414b..e34910a157 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -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", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index b355df0c3a..6aa7378060 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -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", diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index e4fec2dded..0f2523520d 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -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", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index 611317d2e2..356a7156a5 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -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 n’a 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", diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index 363123089f..b77a153142 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -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", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index f228c0eacc..69bffdf78d 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -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": "アップグレード", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index 3f1381b0a3..8cfe983372 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -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", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index ceae3e8480..7038e8b89e 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -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", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 264f6f0739..3d47f754cf 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -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", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 532721cf96..ba8947b0c4 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -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", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index 3dda96f761..c86af9b769 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -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": "Обновить", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index 610790dc3e..fc82abff8f 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -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", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 6205d8fa87..a64de1aea5 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -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": "升级", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index ad1d6bc434..a7b82b77e7 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -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": "升級", diff --git a/apps/web/modules/ee/billing/actions.ts b/apps/web/modules/ee/billing/actions.ts index 97055fb8f8..da33fb7fd6 100644 --- a/apps/web/modules/ee/billing/actions.ts +++ b/apps/web/modules/ee/billing/actions.ts @@ -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 }; }); diff --git a/apps/web/modules/ee/billing/api/lib/create-setup-checkout-session.ts b/apps/web/modules/ee/billing/api/lib/create-setup-checkout-session.ts new file mode 100644 index 0000000000..6750041230 --- /dev/null +++ b/apps/web/modules/ee/billing/api/lib/create-setup-checkout-session.ts @@ -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 => { + 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; +}; diff --git a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts index cdb272b774..21ae08984e 100644 --- a/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts +++ b/apps/web/modules/ee/billing/api/lib/stripe-webhook.ts @@ -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 => { + 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, diff --git a/apps/web/modules/ee/billing/components/pricing-table.tsx b/apps/web/modules/ee/billing/components/pricing-table.tsx index ebf56892a3..584a052f21 100644 --- a/apps/web/modules/ee/billing/components/pricing-table.tsx +++ b/apps/web/modules/ee/billing/components/pricing-table.tsx @@ -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(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 (
+ {trialDaysRemaining !== null && + (organization.billing.stripe?.hasPaymentMethod ? ( + + + {t("environments.settings.billing.trial_payment_method_added_description")} + + + ) : ( + + + {t("environments.settings.billing.trial_alert_description")} + + {hasBillingRights && ( + void openTrialPaymentCheckout()}> + {t("environments.settings.billing.add_payment_method")} + + )} + + ))} {isStripeSetupIncomplete && hasBillingRights && ( {t("environments.settings.billing.stripe_setup_incomplete")} @@ -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 = ({
- {currentCloudPlan === "pro" && ( + {currentCloudPlan === "pro" && !isTrialing && (
diff --git a/apps/web/modules/ee/billing/components/select-plan-card.tsx b/apps/web/modules/ee/billing/components/select-plan-card.tsx index 0614cd9e6b..cdb7e3a8cb 100644 --- a/apps/web/modules/ee/billing/components/select-plan-card.tsx +++ b/apps/web/modules/ee/billing/components/select-plan-card.tsx @@ -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") { diff --git a/apps/web/modules/ee/billing/components/trial-alert.tsx b/apps/web/modules/ee/billing/components/trial-alert.tsx new file mode 100644 index 0000000000..106020f923 --- /dev/null +++ b/apps/web/modules/ee/billing/components/trial-alert.tsx @@ -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 ( + + {title} + {children} + + ); +}; diff --git a/apps/web/modules/ee/billing/lib/cloud-billing-display.test.ts b/apps/web/modules/ee/billing/lib/cloud-billing-display.test.ts index dc17f745e4..6c143c711d 100644 --- a/apps/web/modules/ee/billing/lib/cloud-billing-display.test.ts +++ b/apps/web/modules/ee/billing/lib/cloud-billing-display.test.ts @@ -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, diff --git a/apps/web/modules/ee/billing/lib/cloud-billing-display.ts b/apps/web/modules/ee/billing/lib/cloud-billing-display.ts index b18c888e1e..c2583ee758 100644 --- a/apps/web/modules/ee/billing/lib/cloud-billing-display.ts +++ b/apps/web/modules/ee/billing/lib/cloud-billing-display.ts @@ -10,6 +10,7 @@ export type TCloudBillingDisplayContext = { organizationId: string; currentCloudPlan: TCloudBillingDisplayPlan; currentSubscriptionStatus: TOrganizationStripeSubscriptionStatus | null; + trialDaysRemaining: number | null; usageCycleStart: Date; usageCycleEnd: Date; billing: NonNullable>>; @@ -27,6 +28,22 @@ const resolveCurrentSubscriptionStatus = ( return billing.stripe?.subscriptionStatus ?? null; }; +const MS_PER_DAY = 86_400_000; + +const resolveTrialDaysRemaining = ( + billing: NonNullable>> +): 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 => { @@ -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, diff --git a/apps/web/modules/ee/billing/lib/organization-billing.ts b/apps/web/modules/ee/billing/lib/organization-billing.ts index 371deb6fc6..7114527f32 100644 --- a/apps/web/modules/ee/billing/lib/organization-billing.ts +++ b/apps/web/modules/ee/billing/lib/organization-billing.ts @@ -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 => { +const hasEmailUsedProTrial = async (email: string, proProductId: string): Promise => { 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 => { @@ -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), }, }; diff --git a/apps/web/modules/ee/billing/page.tsx b/apps/web/modules/ee/billing/page.tsx index 991e24d896..e7798f33d4 100644 --- a/apps/web/modules/ee/billing/page.tsx +++ b/apps/web/modules/ee/billing/page.tsx @@ -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} /> ); diff --git a/packages/types/organizations.ts b/packages/types/organizations.ts index 40de19170b..5fc20d4df5 100644 --- a/packages/types/organizations.ts +++ b/packages/types/organizations.ts @@ -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;