From aa9d67887cad03c8c020329bcba91f6ba5d720f0 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Thu, 20 Nov 2025 11:13:38 +0000 Subject: [PATCH] Realy subscription info. --- .../plan/AvailablePlansSection.tsx | 73 +------------- .../plan/FeatureComparisonTable.tsx | 93 +++++++++++++++++ .../config/configSections/plan/PlanCard.tsx | 46 +++++---- .../configSections/plan/StaticPlanSection.tsx | 99 +++++++++---------- .../proprietary/constants/planConstants.ts | 97 ++++++++++++++++++ .../proprietary/services/licenseService.ts | 87 +++------------- 6 files changed, 277 insertions(+), 218 deletions(-) create mode 100644 frontend/src/proprietary/components/shared/config/configSections/plan/FeatureComparisonTable.tsx create mode 100644 frontend/src/proprietary/constants/planConstants.ts diff --git a/frontend/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.tsx index f51e659a0..2c0640a3f 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/plan/AvailablePlansSection.tsx @@ -1,8 +1,9 @@ import React, { useState, useMemo } from 'react'; -import { Button, Card, Badge, Text, Collapse } from '@mantine/core'; +import { Button, Collapse } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import licenseService, { PlanTier, PlanTierGroup, LicenseInfo, mapLicenseToTier } from '@app/services/licenseService'; import PlanCard from '@app/components/shared/config/configSections/plan/PlanCard'; +import FeatureComparisonTable from '@app/components/shared/config/configSections/plan/FeatureComparisonTable'; interface AvailablePlansSectionProps { plans: PlanTier[]; @@ -99,75 +100,7 @@ const AvailablePlansSection: React.FC = ({ - - - {t('plan.featureComparison', 'Feature Comparison')} - - -
- - - - - {groupedPlans.map((group) => ( - - ))} - - - - {groupedPlans[0]?.features.map((_, featureIndex) => ( - - - {groupedPlans.map((group) => ( - - ))} - - ))} - -
- {t('plan.feature.title', 'Feature')} - - {group.name} - {group.popular && ( - - {t('plan.popular', 'Popular')} - - )} -
- {groupedPlans[0].features[featureIndex].name} - - {group.features[featureIndex]?.included ? ( - - ✓ - - ) : ( - - − - - )} -
-
-
+
); diff --git a/frontend/src/proprietary/components/shared/config/configSections/plan/FeatureComparisonTable.tsx b/frontend/src/proprietary/components/shared/config/configSections/plan/FeatureComparisonTable.tsx new file mode 100644 index 000000000..129d59b9f --- /dev/null +++ b/frontend/src/proprietary/components/shared/config/configSections/plan/FeatureComparisonTable.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { Card, Badge, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { PlanFeature } from '@app/services/licenseService'; + +interface PlanWithFeatures { + name: string; + features: PlanFeature[]; + popular?: boolean; + tier?: string; +} + +interface FeatureComparisonTableProps { + plans: PlanWithFeatures[]; +} + +const FeatureComparisonTable: React.FC = ({ plans }) => { + const { t } = useTranslation(); + + return ( + + + {t('plan.featureComparison', 'Feature Comparison')} + + +
+ + + + + {plans.map((plan, index) => ( + + ))} + + + + {plans[0]?.features.map((_, featureIndex) => ( + + + {plans.map((plan, planIndex) => ( + + ))} + + ))} + +
+ {t('plan.feature.title', 'Feature')} + + {plan.name} + {plan.popular && ( + + {t('plan.popular', 'Popular')} + + )} +
+ {plans[0].features[featureIndex].name} + + {plan.features[featureIndex]?.included ? ( + + ✓ + + ) : ( + + − + + )} +
+
+
+ ); +}; + +export default FeatureComparisonTable; diff --git a/frontend/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx b/frontend/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx index fdf11033e..696fa50e9 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/plan/PlanCard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Button, Card, Badge, Text, Group, Stack, Divider } from '@mantine/core'; +import { Button, Card, Badge, Text, Stack, Divider } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { PlanTierGroup, LicenseInfo } from '@app/services/licenseService'; @@ -45,6 +45,9 @@ const PlanCard: React.FC = ({ planGroup, isCurrentTier, isDowngra {planGroup.name} + + {t('plan.from', 'From')} + £0 @@ -53,7 +56,9 @@ const PlanCard: React.FC = ({ planGroup, isCurrentTier, isDowngra - + + + {planGroup.highlights.map((highlight, index) => ( • {highlight} @@ -124,40 +129,33 @@ const PlanCard: React.FC = ({ planGroup, isCurrentTier, isDowngra {/* Tier Name */}
- + {planGroup.name} -
- {/* "From" Pricing */} -
{t('plan.from', 'From')} + {/* Price */} {isEnterprise && displaySeatPrice !== undefined ? ( -
- - - {displayCurrency}{displayPrice} - - - {t('plan.perMonth', '/month')} - - - - + {displayCurrency}{displaySeatPrice}/seat/month - -
- ) : ( - - + <> + {displayCurrency}{displayPrice} - + + + {displayCurrency}{displaySeatPrice}/seat {t('plan.perMonth', '/month')} + + + ) : ( + <> + + {displayCurrency}{displayPrice} + + {t('plan.perMonth', '/month')} - + )} {/* Show seat count for enterprise plans when current */} diff --git a/frontend/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx index bef21168f..3bdd06baf 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx @@ -7,7 +7,9 @@ import { useRestartServer } from '@app/components/shared/config/useRestartServer import { useAdminSettings } from '@app/hooks/useAdminSettings'; import PendingBadge from '@app/components/shared/config/PendingBadge'; import { alert } from '@app/components/toast'; -import { LicenseInfo } from '@app/services/licenseService'; +import { LicenseInfo, mapLicenseToTier } from '@app/services/licenseService'; +import { PLAN_FEATURES, PLAN_HIGHLIGHTS } from '@app/constants/planConstants'; +import FeatureComparisonTable from '@app/components/shared/config/configSections/plan/FeatureComparisonTable'; interface PremiumSettingsData { key?: string; @@ -21,6 +23,7 @@ interface StaticPlanSectionProps { const StaticPlanSection: React.FC = ({ currentLicenseInfo }) => { const { t } = useTranslation(); const [showLicenseKey, setShowLicenseKey] = useState(false); + const [showComparison, setShowComparison] = useState(false); // Premium/License key management const { restartModalOpened, showRestartModal, closeRestartModal, restartServer } = useRestartServer(); @@ -59,40 +62,20 @@ const StaticPlanSection: React.FC = ({ currentLicenseInf name: t('plan.free.name', 'Free'), price: 0, currency: '£', - period: t('plan.period.month', '/month'), - highlights: [ - t('plan.free.highlight1', 'Limited Tool Usage Per week'), - t('plan.free.highlight2', 'Access to all tools'), - t('plan.free.highlight3', 'Community support'), - ], - features: [ - { name: t('plan.feature.pdfTools', 'Basic PDF Tools'), included: true }, - { name: t('plan.feature.fileSize', 'File Size Limit'), included: false }, - { name: t('plan.feature.automation', 'Automate tool workflows'), included: false }, - { name: t('plan.feature.api', 'API Access'), included: false }, - { name: t('plan.feature.priority', 'Priority Support'), included: false }, - ], + period: '', + highlights: PLAN_HIGHLIGHTS.FREE, + features: PLAN_FEATURES.FREE, maxUsers: 5, }, { - id: 'pro', - name: t('plan.pro.name', 'Pro'), - price: 8, - currency: '£', - period: t('plan.period.perUserPerMonth', '/user/month'), - popular: true, - highlights: [ - t('plan.pro.highlight1', 'Unlimited Tool Usage per user'), - t('plan.pro.highlight2', 'Advanced PDF tools'), - t('plan.pro.highlight3', 'No watermarks'), - ], - features: [ - { name: t('plan.feature.pdfTools', 'Basic PDF Tools'), included: true }, - { name: t('plan.feature.fileSize', 'File Size Limit'), included: true }, - { name: t('plan.feature.automation', 'Automate tool workflows'), included: true }, - { name: t('plan.feature.api', 'Weekly API Credits'), included: true }, - { name: t('plan.feature.priority', 'Priority Support'), included: false }, - ], + id: 'server', + name: 'Server', + price: 0, + currency: '', + period: '', + popular: false, + highlights: PLAN_HIGHLIGHTS.SERVER_MONTHLY, + features: PLAN_FEATURES.SERVER, maxUsers: 'Unlimited users', }, { @@ -101,27 +84,17 @@ const StaticPlanSection: React.FC = ({ currentLicenseInf price: 0, currency: '', period: '', - highlights: [ - t('plan.enterprise.highlight1', 'Custom pricing'), - t('plan.enterprise.highlight2', 'Dedicated support'), - t('plan.enterprise.highlight3', 'Latest features'), - ], - features: [ - { name: t('plan.feature.pdfTools', 'Basic PDF Tools'), included: true }, - { name: t('plan.feature.fileSize', 'File Size Limit'), included: true }, - { name: t('plan.feature.automation', 'Automate tool workflows'), included: true }, - { name: t('plan.feature.api', 'Weekly API Credits'), included: true }, - { name: t('plan.feature.priority', 'Priority Support'), included: true }, - ], + highlights: PLAN_HIGHLIGHTS.ENTERPRISE_MONTHLY, + features: PLAN_FEATURES.ENTERPRISE, maxUsers: 'Custom', }, ]; const getCurrentPlan = () => { - if (!currentLicenseInfo) return staticPlans[0]; - if (currentLicenseInfo.licenseType === 'ENTERPRISE') return staticPlans[2]; - if (currentLicenseInfo.maxUsers > 5) return staticPlans[1]; - return staticPlans[0]; + const tier = mapLicenseToTier(currentLicenseInfo || null); + if (tier === 'enterprise') return staticPlans[2]; + if (tier === 'server') return staticPlans[1]; + return staticPlans[0]; // free }; const currentPlan = getCurrentPlan(); @@ -202,9 +175,21 @@ const StaticPlanSection: React.FC = ({ currentLicenseInf position: 'relative', display: 'flex', flexDirection: 'column', + borderColor: plan.id === currentPlan.id ? 'var(--mantine-color-green-6)' : undefined, + borderWidth: plan.id === currentPlan.id ? '2px' : undefined, }} > - {plan.popular && ( + {plan.id === currentPlan.id && ( + + {t('plan.current', 'Current Plan')} + + )} + {plan.popular && plan.id !== currentPlan.id && ( = ({ currentLicenseInf
+ + {/* Feature Comparison Toggle */} +
+ +
+ + {/* Feature Comparison Table */} + + +
diff --git a/frontend/src/proprietary/constants/planConstants.ts b/frontend/src/proprietary/constants/planConstants.ts new file mode 100644 index 000000000..1865238df --- /dev/null +++ b/frontend/src/proprietary/constants/planConstants.ts @@ -0,0 +1,97 @@ +import { PlanFeature } from '@app/services/licenseService'; + +/** + * Shared plan feature definitions for Stirling PDF Self-Hosted + * Used by both dynamic (Stripe) and static (fallback) plan displays + */ + +export const PLAN_FEATURES = { + FREE: [ + { name: 'Self-hosted deployment', included: true }, + { name: 'All PDF operations', included: true }, + { name: 'Secure Login Support', included: true }, + { name: 'Community support', included: true }, + { name: 'Regular updates', included: true }, + { name: 'up to 5 users', included: true }, + { name: 'Unlimited users', included: false }, + { name: 'Google drive integration', included: false }, + { name: 'External Database', included: false }, + { name: 'Editing text in pdfs', included: false }, + { name: 'Users limited to seats', included: false }, + { name: 'SSO', included: false }, + { name: 'Auditing', included: false }, + { name: 'Usage tracking', included: false }, + { name: 'Prometheus Support', included: false }, + { name: 'Custom PDF metadata', included: false }, + ] as PlanFeature[], + + SERVER: [ + { name: 'Self-hosted deployment', included: true }, + { name: 'All PDF operations', included: true }, + { name: 'Secure Login Support', included: true }, + { name: 'Community support', included: true }, + { name: 'Regular updates', included: true }, + { name: 'Up to 5 users', included: false }, + { name: 'Unlimited users', included: true }, + { name: 'Google drive integration', included: true }, + { name: 'External Database', included: true }, + { name: 'Editing text in pdfs', included: true }, + { name: 'Users limited to seats', included: false }, + { name: 'SSO', included: false }, + { name: 'Auditing', included: false }, + { name: 'Usage tracking', included: false }, + { name: 'Prometheus Support', included: false }, + { name: 'Custom PDF metadata', included: false }, + ] as PlanFeature[], + + ENTERPRISE: [ + { name: 'Self-hosted deployment', included: true }, + { name: 'All PDF operations', included: true }, + { name: 'Secure Login Support', included: true }, + { name: 'Community support', included: true }, + { name: 'Regular updates', included: true }, + { name: 'up to 5 users', included: false }, + { name: 'Unlimited users', included: false }, + { name: 'Google drive integration', included: true }, + { name: 'External Database', included: true }, + { name: 'Editing text in pdfs', included: true }, + { name: 'Users limited to seats', included: true }, + { name: 'SSO', included: true }, + { name: 'Auditing', included: true }, + { name: 'Usage tracking', included: true }, + { name: 'Prometheus Support', included: true }, + { name: 'Custom PDF metadata', included: true }, + ] as PlanFeature[], +} as const; + +export const PLAN_HIGHLIGHTS = { + FREE: [ + 'Up to 5 users', + 'Self-hosted', + 'All basic features' + ], + SERVER_MONTHLY: [ + 'Self-hosted on your infrastructure', + 'Unlimited users', + 'Advanced integrations', + 'Cancel anytime' + ], + SERVER_YEARLY: [ + 'Self-hosted on your infrastructure', + 'Unlimited users', + 'Advanced integrations', + 'Save with annual billing' + ], + ENTERPRISE_MONTHLY: [ + 'Enterprise features (SSO, Auditing)', + 'Usage tracking & Prometheus', + 'Custom PDF metadata', + 'Per-seat licensing' + ], + ENTERPRISE_YEARLY: [ + 'Enterprise features (SSO, Auditing)', + 'Usage tracking & Prometheus', + 'Custom PDF metadata', + 'Save with annual billing' + ] +} as const; diff --git a/frontend/src/proprietary/services/licenseService.ts b/frontend/src/proprietary/services/licenseService.ts index 2b5c6c25f..409dbfd74 100644 --- a/frontend/src/proprietary/services/licenseService.ts +++ b/frontend/src/proprietary/services/licenseService.ts @@ -1,6 +1,7 @@ import apiClient from '@app/services/apiClient'; import { supabase, isSupabaseConfigured } from '@app/services/supabaseClient'; import { getCheckoutMode } from '@app/utils/protocolDetection'; +import { PLAN_FEATURES, PLAN_HIGHLIGHTS } from '@app/constants/planConstants'; export interface PlanFeature { name: string; @@ -15,7 +16,7 @@ export interface PlanTier { period: string; popular?: boolean; features: PlanFeature[]; - highlights: string[]; + highlights: readonly string[]; isContactOnly?: boolean; seatPrice?: number; // Per-seat price for enterprise plans requiresSeats?: boolean; // Flag indicating seat selection is needed @@ -28,7 +29,7 @@ export interface PlanTierGroup { monthly: PlanTier | null; yearly: PlanTier | null; features: PlanFeature[]; - highlights: string[]; + highlights: readonly string[]; popular?: boolean; } @@ -170,20 +171,8 @@ const licenseService = { currency: currencySymbol, period: '/month', popular: false, - features: [ - { name: 'Self-hosted deployment', included: true }, - { name: 'All PDF operations', included: true }, - { name: 'Unlimited users', included: true }, - { name: 'Community support', included: true }, - { name: 'Regular updates', included: true }, - { name: 'Priority support', included: false }, - { name: 'Custom integrations', included: false }, - ], - highlights: [ - 'Self-hosted on your infrastructure', - 'All features included', - 'Cancel anytime' - ] + features: PLAN_FEATURES.SERVER, + highlights: PLAN_HIGHLIGHTS.SERVER_MONTHLY }, { id: 'selfhosted:server:yearly', @@ -193,20 +182,8 @@ const licenseService = { currency: currencySymbol, period: '/year', popular: true, - features: [ - { name: 'Self-hosted deployment', included: true }, - { name: 'All PDF operations', included: true }, - { name: 'Unlimited users', included: true }, - { name: 'Community support', included: true }, - { name: 'Regular updates', included: true }, - { name: 'Priority support', included: false }, - { name: 'Custom integrations', included: false }, - ], - highlights: [ - 'Self-hosted on your infrastructure', - 'All features included', - 'Save with annual billing' - ] + features: PLAN_FEATURES.SERVER, + highlights: PLAN_HIGHLIGHTS.SERVER_YEARLY }, { id: 'selfhosted:enterprise:monthly', @@ -218,20 +195,8 @@ const licenseService = { period: '/month', popular: false, requiresSeats: true, - features: [ - { name: 'Self-hosted deployment', included: true }, - { name: 'All PDF operations', included: true }, - { name: 'Per-seat licensing', included: true }, - { name: 'Priority support', included: true }, - { name: 'SLA guarantee', included: true }, - { name: 'Custom integrations', included: true }, - { name: 'Dedicated account manager', included: true }, - ], - highlights: [ - 'Enterprise-grade support', - 'Custom integrations available', - 'SLA guarantee included' - ] + features: PLAN_FEATURES.ENTERPRISE, + highlights: PLAN_HIGHLIGHTS.ENTERPRISE_MONTHLY }, { id: 'selfhosted:enterprise:yearly', @@ -243,20 +208,8 @@ const licenseService = { period: '/year', popular: false, requiresSeats: true, - features: [ - { name: 'Self-hosted deployment', included: true }, - { name: 'All PDF operations', included: true }, - { name: 'Per-seat licensing', included: true }, - { name: 'Priority support', included: true }, - { name: 'SLA guarantee', included: true }, - { name: 'Custom integrations', included: true }, - { name: 'Dedicated account manager', included: true }, - ], - highlights: [ - 'Enterprise-grade support', - 'Custom integrations available', - 'Save with annual billing' - ] + features: PLAN_FEATURES.ENTERPRISE, + highlights: PLAN_HIGHLIGHTS.ENTERPRISE_YEARLY }, ]; @@ -277,22 +230,8 @@ const licenseService = { currency: currencySymbol, period: '', popular: false, - features: [ - { name: 'Self-hosted deployment', included: true }, - { name: 'All PDF operations', included: true }, - { name: 'Up to 5 users', included: true }, - { name: 'Community support', included: true }, - { name: 'Regular updates', included: true }, - { name: 'Priority support', included: false }, - { name: 'SLA guarantee', included: false }, - { name: 'Custom integrations', included: false }, - { name: 'Dedicated account manager', included: false }, - ], - highlights: [ - 'Up to 5 users', - 'Self-hosted', - 'All basic features' - ] + features: PLAN_FEATURES.FREE, + highlights: PLAN_HIGHLIGHTS.FREE }; const allPlans = [freePlan, ...validPlans];