Realy subscription info.

This commit is contained in:
Connor Yoh 2025-11-20 11:13:38 +00:00
parent 3f073556e0
commit aa9d67887c
6 changed files with 277 additions and 218 deletions

View File

@ -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<AvailablePlansSectionProps> = ({
</div>
<Collapse in={showComparison}>
<Card padding="lg" radius="md" withBorder style={{ marginTop: '1rem' }}>
<Text size="lg" fw={600} mb="md">
{t('plan.featureComparison', 'Feature Comparison')}
</Text>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '2px solid var(--mantine-color-gray-3)' }}>
<th style={{ textAlign: 'left', padding: '0.75rem' }}>
{t('plan.feature.title', 'Feature')}
</th>
{groupedPlans.map((group) => (
<th
key={group.tier}
style={{
textAlign: 'center',
padding: '0.75rem',
minWidth: '8rem',
position: 'relative'
}}
>
{group.name}
{group.popular && (
<Badge
color="blue"
variant="filled"
size="xs"
style={{
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
}}
>
{t('plan.popular', 'Popular')}
</Badge>
)}
</th>
))}
</tr>
</thead>
<tbody>
{groupedPlans[0]?.features.map((_, featureIndex) => (
<tr
key={featureIndex}
style={{ borderBottom: '1px solid var(--mantine-color-gray-3)' }}
>
<td style={{ padding: '0.75rem' }}>
{groupedPlans[0].features[featureIndex].name}
</td>
{groupedPlans.map((group) => (
<td key={group.tier} style={{ textAlign: 'center', padding: '0.75rem' }}>
{group.features[featureIndex]?.included ? (
<Text c="green" fw={600} size="lg">
</Text>
) : (
<Text c="gray" size="sm">
</Text>
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Card>
<FeatureComparisonTable plans={groupedPlans} />
</Collapse>
</div>
);

View File

@ -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<FeatureComparisonTableProps> = ({ plans }) => {
const { t } = useTranslation();
return (
<Card padding="lg" radius="md" withBorder style={{ marginTop: '1rem' }}>
<Text size="lg" fw={600} mb="md">
{t('plan.featureComparison', 'Feature Comparison')}
</Text>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '2px solid var(--mantine-color-gray-3)' }}>
<th style={{ textAlign: 'left', padding: '0.75rem' }}>
{t('plan.feature.title', 'Feature')}
</th>
{plans.map((plan, index) => (
<th
key={plan.tier || plan.name || index}
style={{
textAlign: 'center',
padding: '0.75rem',
minWidth: '8rem',
position: 'relative'
}}
>
{plan.name}
{plan.popular && (
<Badge
color="blue"
variant="filled"
size="xs"
style={{
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
}}
>
{t('plan.popular', 'Popular')}
</Badge>
)}
</th>
))}
</tr>
</thead>
<tbody>
{plans[0]?.features.map((_, featureIndex) => (
<tr
key={featureIndex}
style={{ borderBottom: '1px solid var(--mantine-color-gray-3)' }}
>
<td style={{ padding: '0.75rem' }}>
{plans[0].features[featureIndex].name}
</td>
{plans.map((plan, planIndex) => (
<td key={planIndex} style={{ textAlign: 'center', padding: '0.75rem' }}>
{plan.features[featureIndex]?.included ? (
<Text c="green" fw={600} size="lg">
</Text>
) : (
<Text c="gray" size="sm">
</Text>
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Card>
);
};
export default FeatureComparisonTable;

View File

@ -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<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngra
<Text size="xl" fw={700} mb="xs">
{planGroup.name}
</Text>
<Text size="xs" c="dimmed" mb="xs" style={{ opacity: 0 }}>
{t('plan.from', 'From')}
</Text>
<Text size="2.5rem" fw={700} style={{ lineHeight: 1 }}>
£0
</Text>
@ -53,7 +56,9 @@ const PlanCard: React.FC<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngra
</Text>
</div>
<Stack gap="xs" mt="md">
<Divider />
<Stack gap="xs">
{planGroup.highlights.map((highlight, index) => (
<Text key={index} size="sm" c="dimmed">
{highlight}
@ -124,40 +129,33 @@ const PlanCard: React.FC<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngra
<Stack gap="md" style={{ height: '100%' }}>
{/* Tier Name */}
<div>
<Text size="xl" fw={700}>
<Text size="xl" fw={700} mb="xs">
{planGroup.name}
</Text>
</div>
{/* "From" Pricing */}
<div>
<Text size="xs" c="dimmed" mb="xs">
{t('plan.from', 'From')}
</Text>
{/* Price */}
{isEnterprise && displaySeatPrice !== undefined ? (
<div>
<Group gap="xs" align="baseline">
<Text size="xl" fw={700}>
{displayCurrency}{displayPrice}
</Text>
<Text size="sm" c="dimmed">
{t('plan.perMonth', '/month')}
</Text>
</Group>
<Text size="sm" c="dimmed">
+ {displayCurrency}{displaySeatPrice}/seat/month
</Text>
</div>
) : (
<Group gap="xs" align="baseline">
<Text size="xl" fw={700}>
<>
<Text size="2.5rem" fw={700} style={{ lineHeight: 1 }}>
{displayCurrency}{displayPrice}
</Text>
<Text size="sm" c="dimmed">
<Text size="sm" c="dimmed" mt="xs">
+ {displayCurrency}{displaySeatPrice}/seat {t('plan.perMonth', '/month')}
</Text>
</>
) : (
<>
<Text size="2.5rem" fw={700} style={{ lineHeight: 1 }}>
{displayCurrency}{displayPrice}
</Text>
<Text size="sm" c="dimmed" mt="xs">
{t('plan.perMonth', '/month')}
</Text>
</Group>
</>
)}
{/* Show seat count for enterprise plans when current */}

View File

@ -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<StaticPlanSectionProps> = ({ 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<StaticPlanSectionProps> = ({ 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<StaticPlanSectionProps> = ({ 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<StaticPlanSectionProps> = ({ 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 && (
<Badge
color="green"
variant="filled"
size="sm"
style={{ position: 'absolute', top: '1rem', right: '1rem' }}
>
{t('plan.current', 'Current Plan')}
</Badge>
)}
{plan.popular && plan.id !== currentPlan.id && (
<Badge
variant="filled"
size="xs"
@ -251,7 +236,7 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({ currentLicenseInf
<div style={{ flexGrow: 1 }} />
<Button
variant={plan.id === currentPlan.id ? 'filled' : 'outline'}
variant={plan.id === currentPlan.id ? 'light' : 'filled'}
disabled={plan.id === currentPlan.id}
fullWidth
onClick={() =>
@ -266,6 +251,20 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({ currentLicenseInf
</Card>
))}
</div>
{/* Feature Comparison Toggle */}
<div style={{ textAlign: 'center', marginTop: '1rem' }}>
<Button variant="subtle" onClick={() => setShowComparison(!showComparison)}>
{showComparison
? t('plan.hideComparison', 'Hide Feature Comparison')
: t('plan.showComparison', 'Compare All Features')}
</Button>
</div>
{/* Feature Comparison Table */}
<Collapse in={showComparison}>
<FeatureComparisonTable plans={staticPlans} />
</Collapse>
</div>
<Divider />

View File

@ -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;

View File

@ -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];