3) Stripe integration

Install (server):

npm i stripe


server/stripe/coverCheckout.ts

import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-06-20" });

// Returns a Price (creates Product/Price if missing)
export async function ensurePriceForTemplate(tplId: string, title: string, amount_cents: number, currency: string) {
  const product = await stripe.products.create({
    name: `Cover Template • ${title}`,
    id: `ct_${tplId}`, // deterministic
  }).catch(async (e) => {
    if (e.raw?.code === "resource_already_exists") {
      return await stripe.products.retrieve(`ct_${tplId}`);
    }
    throw e;
  });

  // Try to find existing active price with same unit_amount
  const prices = await stripe.prices.list({ product: product.id, active: true, type: "one_time", limit: 100 });
  const existing = prices.data.find(p => p.unit_amount === amount_cents && p.currency === currency);
  if (existing) return existing;

  return await stripe.prices.create({
    product: product.id,
    unit_amount: amount_cents,
    currency,
  });
}

export async function createCheckoutSession(args: {
  priceId: string; successUrl: string; cancelUrl: string;
  metadata?: Record<string, string>;
}) {
  return await stripe.checkout.sessions.create({
    mode: "payment",
    line_items: [{ price: args.priceId, quantity: 1 }],
    success_url: args.successUrl,
    cancel_url: args.cancelUrl,
    metadata: args.metadata,
  });
}


Webhook (marks purchase paid):
server/routes.stripeWebhook.ts

import express from "express";
import Stripe from "stripe";
import { markPurchasePaidBySession, getCoverTemplate } from "../storage/coverTemplates";
const router = express.Router();

router.post("/api/stripe/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const sig = req.headers["stripe-signature"];
  const whSecret = process.env.STRIPE_WEBHOOK_SECRET!;
  let event: Stripe.Event;

  try {
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-06-20" });
    event = stripe.webhooks.constructEvent(req.body, sig as string, whSecret);
  } catch (err: any) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  try {
    if (event.type === "checkout.session.completed") {
      const session = event.data.object as Stripe.Checkout.Session;
      const tplId = session.metadata?.templateId!;
      const customImageUrl = session.metadata?.customImageUrl || null;

      const tpl = await getCoverTemplate(tplId);
      const download = tpl?.download_bundle_url || tpl?.pptx_url || tpl?.keynote_url || tpl?.gslides_url || "";
      await markPurchasePaidBySession(session.id, session.payment_intent as string, download);
    }
  } catch (e) {
    console.error(e);
  }
  res.json({ received: true });
});

export default router;


Wire routes (server index):

import coverTemplatesRouter from "./routes.coverTemplates";
import stripeWebhookRouter from "./routes.stripeWebhook";
app.use("/api/stripe/webhook", stripeWebhookRouter); // raw body route
app.use(express.json());                               // after webhook
app.use(coverTemplatesRouter);
