Files
Stirling-PDF/frontend/src/proprietary/services/licenseService.ts
2025-11-17 15:30:04 +00:00

499 lines
15 KiB
TypeScript

import apiClient from './apiClient';
import { supabase } from './supabaseClient';
export interface PlanFeature {
name: string;
included: boolean;
}
export interface PlanTier {
id: string;
name: string;
price: number;
currency: string;
period: string;
popular?: boolean;
features: PlanFeature[];
highlights: string[];
isContactOnly?: boolean;
seatPrice?: number; // Per-seat price for enterprise plans
requiresSeats?: boolean; // Flag indicating seat selection is needed
lookupKey: string; // Stripe lookup key for this plan
}
export interface PlanTierGroup {
tier: 'free' | 'server' | 'enterprise';
name: string;
monthly: PlanTier | null;
yearly: PlanTier | null;
features: PlanFeature[];
highlights: string[];
popular?: boolean;
}
export interface SubscriptionInfo {
plan: PlanTier;
status: 'active' | 'past_due' | 'canceled' | 'incomplete' | 'trialing' | 'none';
currentPeriodEnd?: string;
cancelAtPeriodEnd?: boolean;
stripeCustomerId?: string;
stripeSubscriptionId?: string;
}
export interface PlansResponse {
plans: PlanTier[];
currentSubscription: SubscriptionInfo | null;
}
export interface CheckoutSessionRequest {
lookup_key: string; // Stripe lookup key (e.g., 'selfhosted:server:monthly')
installation_id?: string; // Installation ID from backend (MAC-based fingerprint)
current_license_key?: string; // Current license key for upgrades
requires_seats?: boolean; // Whether to add adjustable seat pricing
seat_count?: number; // Initial number of seats for enterprise plans (user can adjust in Stripe UI)
successUrl?: string;
cancelUrl?: string;
}
export interface CheckoutSessionResponse {
clientSecret: string;
sessionId: string;
}
export interface BillingPortalResponse {
url: string;
}
export interface InstallationIdResponse {
installationId: string;
}
export interface LicenseKeyResponse {
status: 'ready' | 'pending';
license_key?: string;
email?: string;
plan?: string;
}
export interface LicenseInfo {
licenseType: 'NORMAL' | 'PRO' | 'ENTERPRISE';
enabled: boolean;
maxUsers: number;
hasKey: boolean;
licenseKey?: string; // The actual license key (for upgrades)
}
// Currency symbol mapping
const getCurrencySymbol = (currency: string): string => {
const currencySymbols: { [key: string]: string } = {
'gbp': '£',
'usd': '$',
'eur': '€',
'cny': '¥',
'inr': '₹',
'brl': 'R$',
'idr': 'Rp'
};
return currencySymbols[currency.toLowerCase()] || currency.toUpperCase();
};
// Self-hosted plan lookup keys
const SELF_HOSTED_LOOKUP_KEYS = [
'selfhosted:server:monthly',
'selfhosted:server:yearly',
'selfhosted:enterpriseseat:monthly',
'selfhosted:enterpriseseat:yearly',
];
const licenseService = {
/**
* Get available plans with pricing for the specified currency
*/
async getPlans(currency: string = 'gbp'): Promise<PlansResponse> {
try {
// Fetch all self-hosted prices from Stripe
const { data, error } = await supabase.functions.invoke<{
prices: Record<string, {
unit_amount: number;
currency: string;
lookup_key: string;
}>;
missing: string[];
}>('stripe-price-lookup', {
body: {
lookup_keys: SELF_HOSTED_LOOKUP_KEYS,
currency
},
});
if (error) {
throw new Error(`Failed to fetch plans: ${error.message}`);
}
if (!data || !data.prices) {
throw new Error('No pricing data returned');
}
// Log missing prices for debugging
if (data.missing && data.missing.length > 0) {
console.warn('Missing Stripe prices for lookup keys:', data.missing, 'in currency:', currency);
}
// Build price map for easy access
const priceMap = new Map<string, { unit_amount: number; currency: string }>();
for (const [lookupKey, priceData] of Object.entries(data.prices)) {
priceMap.set(lookupKey, {
unit_amount: priceData.unit_amount,
currency: priceData.currency
});
}
const currencySymbol = getCurrencySymbol(currency);
// Helper to get price info
const getPriceInfo = (lookupKey: string, fallback: number = 0) => {
const priceData = priceMap.get(lookupKey);
return priceData ? priceData.unit_amount / 100 : fallback;
};
// Build plan tiers
const plans: PlanTier[] = [
{
id: 'selfhosted:server:monthly',
lookupKey: 'selfhosted:server:monthly',
name: 'Server - Monthly',
price: getPriceInfo('selfhosted:server:monthly'),
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'
]
},
{
id: 'selfhosted:server:yearly',
lookupKey: 'selfhosted:server:yearly',
name: 'Server - Yearly',
price: getPriceInfo('selfhosted:server:yearly'),
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'
]
},
{
id: 'selfhosted:enterprise:monthly',
lookupKey: 'selfhosted:server:monthly',
name: 'Enterprise - Monthly',
price: getPriceInfo('selfhosted:server:monthly'),
seatPrice: getPriceInfo('selfhosted:enterpriseseat:monthly'),
currency: currencySymbol,
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'
]
},
{
id: 'selfhosted:enterprise:yearly',
lookupKey: 'selfhosted:server:yearly',
name: 'Enterprise - Yearly',
price: getPriceInfo('selfhosted:server:yearly'),
seatPrice: getPriceInfo('selfhosted:enterpriseseat:yearly'),
currency: currencySymbol,
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'
]
},
];
// Filter out plans with missing prices (price === 0 means Stripe price not found)
const validPlans = plans.filter(plan => plan.price > 0);
if (validPlans.length < plans.length) {
const missingPlans = plans.filter(plan => plan.price === 0).map(p => p.id);
console.warn('Filtered out plans with missing prices:', missingPlans);
}
// Add Free plan (static definition)
const freePlan: PlanTier = {
id: 'free',
lookupKey: 'free',
name: 'Free',
price: 0,
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'
]
};
const allPlans = [freePlan, ...validPlans];
return {
plans: allPlans,
currentSubscription: null // Will be implemented later
};
} catch (error) {
console.error('Error fetching plans:', error);
throw error;
}
},
/**
* Get current subscription details
* TODO: Implement with Supabase edge function when available
*/
async getCurrentSubscription(): Promise<SubscriptionInfo | null> {
// Placeholder - will be implemented later
return null;
},
/**
* Group plans by tier for display (Free, Server, Enterprise)
*/
groupPlansByTier(plans: PlanTier[]): PlanTierGroup[] {
const groups: PlanTierGroup[] = [];
// Free tier
const freePlan = plans.find(p => p.id === 'free');
if (freePlan) {
groups.push({
tier: 'free',
name: 'Free',
monthly: freePlan,
yearly: null,
features: freePlan.features,
highlights: freePlan.highlights,
popular: false,
});
}
// Server tier
const serverMonthly = plans.find(p => p.lookupKey === 'selfhosted:server:monthly');
const serverYearly = plans.find(p => p.lookupKey === 'selfhosted:server:yearly');
if (serverMonthly || serverYearly) {
groups.push({
tier: 'server',
name: 'Server',
monthly: serverMonthly || null,
yearly: serverYearly || null,
features: (serverMonthly || serverYearly)!.features,
highlights: (serverMonthly || serverYearly)!.highlights,
popular: serverYearly?.popular || serverMonthly?.popular || false,
});
}
// Enterprise tier (uses server pricing + seats)
const enterpriseMonthly = plans.find(p => p.id === 'selfhosted:enterprise:monthly');
const enterpriseYearly = plans.find(p => p.id === 'selfhosted:enterprise:yearly');
if (enterpriseMonthly || enterpriseYearly) {
groups.push({
tier: 'enterprise',
name: 'Enterprise',
monthly: enterpriseMonthly || null,
yearly: enterpriseYearly || null,
features: (enterpriseMonthly || enterpriseYearly)!.features,
highlights: (enterpriseMonthly || enterpriseYearly)!.highlights,
popular: false,
});
}
return groups;
},
/**
* Create a Stripe checkout session for upgrading
*/
async createCheckoutSession(request: CheckoutSessionRequest): Promise<CheckoutSessionResponse> {
const { data, error } = await supabase.functions.invoke('create-checkout', {
body: {
self_hosted: true,
lookup_key: request.lookup_key,
installation_id: request.installation_id,
current_license_key: request.current_license_key,
requires_seats: request.requires_seats,
seat_count: request.seat_count || 1,
callback_base_url: window.location.origin,
},
});
if (error) {
throw new Error(`Failed to create checkout session: ${error.message}`);
}
return data as CheckoutSessionResponse;
},
/**
* Create a Stripe billing portal session for managing subscription
*/
async createBillingPortalSession(email: string, returnUrl: string): Promise<BillingPortalResponse> {
const { data, error} = await supabase.functions.invoke('manage-billing', {
body: {
email,
returnUrl
},
});
if (error) {
throw new Error(`Failed to create billing portal session: ${error.message}`);
}
return data as BillingPortalResponse;
},
/**
* Get the installation ID from the backend (MAC-based fingerprint)
*/
async getInstallationId(): Promise<string> {
try {
const response = await apiClient.get('/api/v1/admin/installation-id');
const data: InstallationIdResponse = await response.data;
return data.installationId;
} catch (error) {
console.error('Error fetching installation ID:', error);
throw error;
}
},
/**
* Check if license key is ready for the given installation ID
*/
async checkLicenseKey(installationId: string): Promise<LicenseKeyResponse> {
const { data, error } = await supabase.functions.invoke('get-license-key', {
body: {
installation_id: installationId,
},
});
if (error) {
throw new Error(`Failed to check license key: ${error.message}`);
}
return data as LicenseKeyResponse;
},
/**
* Save license key to backend
*/
async saveLicenseKey(licenseKey: string): Promise<{success: boolean; licenseType?: string; message?: string; error?: string}> {
try {
const response = await apiClient.post('/api/v1/admin/license-key', {
licenseKey: licenseKey,
});
return response.data;
} catch (error) {
console.error('Error saving license key:', error);
throw error;
}
},
/**
* Get current license information from backend
*/
async getLicenseInfo(): Promise<LicenseInfo> {
try {
const response = await apiClient.get('/api/v1/admin/license-info');
return response.data;
} catch (error) {
console.error('Error fetching license info:', error);
throw error;
}
},
};
/**
* Map license type to plan tier
* @param licenseInfo - Current license information
* @returns Plan tier: 'free' | 'server' | 'enterprise'
*/
export const mapLicenseToTier = (licenseInfo: LicenseInfo | null): 'free' | 'server' | 'enterprise' | null => {
if (!licenseInfo) return null;
// No license or NORMAL type = Free tier
if (licenseInfo.licenseType === 'NORMAL' || !licenseInfo.enabled) {
return 'free';
}
// PRO type (no seats) = Server tier
if (licenseInfo.licenseType === 'PRO') {
return 'server';
}
// ENTERPRISE type (with seats) = Enterprise tier
if (licenseInfo.licenseType === 'ENTERPRISE' && licenseInfo.maxUsers > 0) {
return 'enterprise';
}
// Default fallback
return 'free';
};
export default licenseService;