bookwiz.io / lib / stripe.ts
stripe.ts
Raw
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;
}