import { loadStripe } from '@stripe/stripe-js';
import Stripe from 'stripe';
import { SparklesIcon, RocketLaunchIcon, UserGroupIcon, StarIcon, DocumentTextIcon } from '@heroicons/react/24/outline';
import { validateStripeEnv } from './env-validation';
// Initialize Stripe on the server side only
let stripe: Stripe | null = null;
if (typeof window === 'undefined') {
try {
const config = validateStripeEnv();
stripe = new Stripe(config.STRIPE_SECRET_KEY!, {
apiVersion: '2025-05-28.basil',
typescript: true,
});
} catch (error) {
console.error('Failed to initialize server-side Stripe:', error);
}
}
// Initialize Stripe on the client side
let stripePromise: Promise<any> | null = null;
export const getStripe = () => {
if (typeof window === 'undefined') return null;
if (!stripePromise) {
const config = validateStripeEnv();
stripePromise = loadStripe(config.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
}
return stripePromise;
};
// Export the server-side Stripe instance
export { stripe };
export type BillingInterval = 'month' | 'year';
// Environment-specific price IDs
const isProduction = process.env.NODE_ENV === 'production';
const PRICE_IDS = {
EXPLORER: {
monthly: isProduction ? 'price_1NXtg7Jf6tol2iFRXqG1nUiV' : 'price_1RWxDvJf6tol2iFRdfnnvAKE',
annually: isProduction ? 'price_1NXtg8Jf6tol2iFRcx8QZyPU' : 'price_1RWxDvJf6tol2iFRil3C7eKB',
},
STORYTELLER: {
monthly: isProduction ? 'price_1NXtuFJf6tol2iFRRJJg3pDN' : 'price_1RWxEpJf6tol2iFRwl5HJraX',
annually: isProduction ? 'price_1NXtuFJf6tol2iFR7SpaNHcz' : 'price_1RWxEpJf6tol2iFRZAlz7zaY',
},
PROFESSIONAL: {
monthly: isProduction ? 'price_1NF2phJf6tol2iFRcdzQkGV5' : 'price_1RWxFQJf6tol2iFRGxE9JQcG',
annually: isProduction ? 'price_1NkB3DJf6tol2iFRRzOqr9JR' : 'price_1RWxFiJf6tol2iFRs0eFTe1h',
},
};
export interface PricingTier {
id: string;
name: string;
description: string;
features: string[];
cta: string;
popular: boolean;
icon: typeof SparklesIcon;
color: string;
borderColor: string;
// Benefit details (consolidated from PRICING_TIER_BENEFITS)
maxSmartPrompts: number;
hasUnlimitedFastPrompts: boolean;
maxFastPrompts: number;
maxStorageGB: number;
canCreateCustomTemplates: boolean;
maxCustomTemplates?: number; // undefined means unlimited
maxBooks?: number; // undefined means unlimited
hasPrioritySupport: boolean;
hasAdvancedExports: boolean;
hasCollaboration: boolean;
hasAPIAccess: boolean;
resetPeriod: 'monthly' | 'annually';
pricing: {
monthly: {
price: number;
priceId: string;
};
annually: {
price: number;
priceId: string;
savings: number; // How much they save per month
};
};
}
// Define the pricing tiers with environment-specific Stripe price IDs
export const PRICING_TIERS: Record<string, PricingTier> = {
FREE: {
id: 'free',
name: 'Free',
description: 'Get started with basic features',
features: [
'Create up to 3 books',
'3 prompts to 🧠 Smart AI models',
'15 prompts to ⚡ Fast AI models',
'Cloud storage (100MB)',
'Community support',
],
cta: 'Get Started Free',
popular: false,
icon: DocumentTextIcon,
color: 'from-gray-600 to-gray-700',
borderColor: 'border-gray-600/50',
maxSmartPrompts: 3,
hasUnlimitedFastPrompts: false,
maxFastPrompts: 15,
maxStorageGB: 0.1,
canCreateCustomTemplates: false,
maxCustomTemplates: 0,
maxBooks: 3,
hasPrioritySupport: false,
hasAdvancedExports: false,
hasCollaboration: false,
hasAPIAccess: false,
resetPeriod: 'monthly',
pricing: {
monthly: {
price: 0,
priceId: 'free',
},
annually: {
price: 0,
priceId: 'free',
savings: 0,
},
},
},
EXPLORER: {
id: 'explorer',
name: 'Explorer',
description: 'Perfect for writers just starting their journey',
features: [
'Unlimited writing projects',
'100 🧠 Smart AI prompts',
'1000 ⚡ Fast AI prompts',
'Cloud storage (1GB)',
'Standard support',
],
cta: 'Start Writing',
popular: false,
icon: SparklesIcon,
color: 'from-teal-600 to-cyan-600',
borderColor: 'border-teal-600/50',
maxSmartPrompts: 100,
hasUnlimitedFastPrompts: false,
maxFastPrompts: 1000,
maxStorageGB: 1,
canCreateCustomTemplates: false,
maxCustomTemplates: 0,
maxBooks: undefined, // unlimited
hasPrioritySupport: false,
hasAdvancedExports: false,
hasCollaboration: false,
hasAPIAccess: false,
resetPeriod: 'monthly',
pricing: {
monthly: {
price: 9,
priceId: PRICE_IDS.EXPLORER.monthly,
},
annually: {
price: 90,
priceId: PRICE_IDS.EXPLORER.annually,
savings: 1.5, // $9 * 12 = $108, they pay $90, save $18/year = $1.5/month
},
},
},
STORYTELLER: {
id: 'storyteller',
name: 'Storyteller',
description: 'For serious writers crafting their masterpieces',
features: [
'Everything in Explorer',
'500 🧠 Smart AI prompts',
'Unlimited ⚡ Fast AI prompts',
'Cloud storage (10GB)',
'Priority support',
],
cta: 'Upgrade to Storyteller',
popular: true,
icon: RocketLaunchIcon,
color: 'from-cyan-500 to-teal-500',
borderColor: 'border-cyan-500/50',
maxSmartPrompts: 500,
hasUnlimitedFastPrompts: true,
maxFastPrompts: 999999,
maxStorageGB: 10,
canCreateCustomTemplates: true,
maxCustomTemplates: undefined, // unlimited once you can create them
maxBooks: undefined, // unlimited
hasPrioritySupport: true,
hasAdvancedExports: true,
hasCollaboration: false,
hasAPIAccess: false,
resetPeriod: 'monthly',
pricing: {
monthly: {
price: 35,
priceId: PRICE_IDS.STORYTELLER.monthly,
},
annually: {
price: 350,
priceId: PRICE_IDS.STORYTELLER.annually,
savings: 5, // $35 * 12 = $420, they pay $350, save $70/year = $5.83/month
},
},
},
PROFESSIONAL: {
id: 'professional',
name: 'Professional',
description: 'For professional authors and publishing teams',
features: [
'Everything in Storyteller',
'2000 🧠 Smart AI prompts',
'Unlimited ⚡ Fast AI prompts',
'Cloud storage (40GB)',
'24/7 priority support',
],
cta: 'Upgrade to Professional',
popular: false,
icon: StarIcon,
color: 'from-amber-500 to-orange-500',
borderColor: 'border-amber-500/50',
maxSmartPrompts: 2000,
hasUnlimitedFastPrompts: true,
maxFastPrompts: 999999,
maxStorageGB: 20,
canCreateCustomTemplates: true,
maxCustomTemplates: undefined, // unlimited once you can create them
maxBooks: undefined, // unlimited
hasPrioritySupport: true,
hasAdvancedExports: true,
hasCollaboration: true,
hasAPIAccess: true,
resetPeriod: 'monthly',
pricing: {
monthly: {
price: 99,
priceId: PRICE_IDS.PROFESSIONAL.monthly,
},
annually: {
price: 990,
priceId: PRICE_IDS.PROFESSIONAL.annually,
savings: 9, // $99 * 12 = $1188, they pay $990, save $198/year = $16.5/month
},
},
},
};
// Utility functions
export function getPlanByPriceId(priceId: string) {
return Object.values(PRICING_TIERS).find(tier =>
tier.pricing.monthly.priceId === priceId ||
tier.pricing.annually.priceId === priceId
) || null;
}
export function formatPrice(price: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
}).format(price);
}
export function formatDate(dateString: string | null | undefined): string {
if (!dateString) {
return 'Not available';
}
const date = new Date(dateString);
// Check if date is valid
if (isNaN(date.getTime())) {
return 'Invalid date';
}
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}
export function getPriceForInterval(tier: PricingTier, interval: BillingInterval) {
return interval === 'month' ? tier.pricing.monthly : tier.pricing.annually;
}
export function calculateMonthlySavings(tier: PricingTier): number {
const monthlyPrice = tier.pricing.monthly.price;
const annualMonthlyPrice = tier.pricing.annually.price / 12;
return monthlyPrice - annualMonthlyPrice;
}
// Create a map of price IDs to tier IDs for quick lookup
const PRICE_ID_TO_TIER_MAP: Record<string, string> = {};
Object.values(PRICING_TIERS).forEach(tier => {
PRICE_ID_TO_TIER_MAP[tier.pricing.monthly.priceId] = tier.id.toUpperCase();
PRICE_ID_TO_TIER_MAP[tier.pricing.annually.priceId] = tier.id.toUpperCase();
});
// Utility function to get tier by price ID
export function getTierByPriceId(priceId: string | null): PricingTier {
if (!priceId || priceId === 'free') return PRICING_TIERS.FREE;
const tierId = PRICE_ID_TO_TIER_MAP[priceId];
return PRICING_TIERS[tierId] || PRICING_TIERS.FREE;
}
// Utility function to get tier by tier ID
export function getTierById(tierId: string | null): PricingTier {
if (!tierId) return PRICING_TIERS.FREE;
return PRICING_TIERS[tierId.toUpperCase()] || PRICING_TIERS.FREE;
}