diff --git a/api/checkout.js b/api/checkout.js index e607942..4ce4994 100644 --- a/api/checkout.js +++ b/api/checkout.js @@ -1,141 +1,98 @@ /** * Stripe Checkout Session API - * Deploy as a Cloudflare Worker, Vercel function, or standalone server. + * Deploy as standalone Node.js server. * - * Environment variables needed: - * - STRIPE_SECRET_KEY: sk_live_... or sk_test_... - * - FRONTEND_URL: https://git.fdsa.agency (or your domain) - * - * Usage: - * POST /api/checkout - * Body: { "price_id": "price_abc123", "mode": "payment", "customer_email": "..." } + * Environment: STRIPE_SECRET_KEY, FRONTEND_URL */ -const stripeEndpoint = 'https://api.stripe.com/v1/checkout/sessions' +const http = require('http') +const https = require('https') -async function handleRequest(request) { - // CORS for frontend - if (request.method === 'OPTIONS') { - return new Response(null, { - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }, - }) - } +const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY +const FRONTEND_URL = process.env.FRONTEND_URL || 'https://git.fdsa.agency' +const PORT = process.env.PORT || 8787 - if (request.method !== 'POST') { - return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405 }) - } - - const stripeKey = globalThis.STRIPE_SECRET_KEY || process?.env?.STRIPE_SECRET_KEY - const frontendUrl = globalThis.FRONTEND_URL || process?.env?.FRONTEND_URL || 'https://git.fdsa.agency' - - if (!stripeKey) { - return new Response(JSON.stringify({ error: 'Stripe not configured' }), { - status: 500, - headers: { 'Access-Control-Allow-Origin': '*' }, - }) - } - - try { - const { price_id, mode, customer_email } = await request.json() - - const body = new URLSearchParams({ - 'mode': mode || 'payment', - 'success_url': `${frontendUrl}/checkout/success?session_id={CHECKOUT_SESSION_ID}`, - 'cancel_url': `${frontendUrl}/checkout/cancel`, - 'allow_promotion_codes': 'true', - 'line_items[0][price]': price_id, +function createCheckoutSession(priceId, mode, email) { + return new Promise((resolve, reject) => { + const data = new URLSearchParams({ + mode: mode || 'payment', + success_url: `${FRONTEND_URL}/checkout/success`, + cancel_url: `${FRONTEND_URL}/checkout/cancel`, + allow_promotion_codes: 'true', + 'line_items[0][price]': priceId, 'line_items[0][quantity]': '1', }) + if (email) data.set('customer_email', email) - if (customer_email) { - body.set('customer_email', customer_email) - } - - const stripeResp = await fetch(stripeEndpoint, { + const options = { + hostname: 'api.stripe.com', + path: '/v1/checkout/sessions', method: 'POST', headers: { - 'Authorization': `Bearer ${stripeKey}`, + 'Authorization': `Bearer ${STRIPE_SECRET_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': data.toString().length, }, - body: body.toString(), - }) - - const data = await stripeJson(stripeResp) - - if (!stripeResp.ok) { - throw new Error(data.error?.message || 'Stripe error') } - return new Response(JSON.stringify({ url: data.url, session_id: data.id }), { - status: 200, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - }) - } catch (e) { - return new Response(JSON.stringify({ error: e.message }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }, - }) - } -} - -async function stripeJson(response) { - const text = await response.text() - try { - return JSON.parse(text) - } catch { - // Stripe sometimes returns unexpected formats — parse query string - const params = new URLSearchParams(text) - const obj = {} - for (const [k, v] of params) { - // Handle nested keys like 'error.message' - const parts = k.split('.') - if (parts.length > 1) { - let cursor = obj - for (let i = 0; i < parts.length - 1; i++) { - cursor[parts[i]] = cursor[parts[i]] || {} - cursor = cursor[parts[i]] - } - cursor[parts[parts.length - 1]] = v - } else { - obj[k] = v - } - } - return obj - } -} - -// Cloudflare Worker entry -export default { fetch: handleRequest } - -// Node.js entry -if (typeof module !== 'undefined' && require?.main === module) { - const http = require('http') - const server = http.createServer((req, res) => { - // Minimal Node.js adapter — assumes body is JSON - let body = '' - req.on('data', chunk => body += chunk) - req.on('end', async () => { - globalThis.process = process - globalThis.request = new Request(`http://localhost${req.url}`, { - method: req.method, - headers: req.headers, - body: body || undefined, + const req = https.request(options, (res) => { + let body = '' + res.on('data', chunk => body += chunk) + res.on('end', () => { + try { resolve({ status: res.statusCode, body: JSON.parse(body) }) } + catch { resolve({ status: res.statusCode, body: { error: { message: body } } }) } }) - const response = await handleRequest(globalThis.request) - res.writeHead(response.status, Object.fromEntries(response.headers)) - res.end(await response.text()) }) + req.on('error', reject) + req.write(data.toString()) + req.end() }) - const port = process.env.PORT || 8787 - server.listen(port, () => console.log(`Checkout API on :${port}`)) } + +const server = http.createServer(async (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type') + + if (req.method === 'OPTIONS') { + res.writeHead(204) + res.end() + return + } + + if (req.method !== 'POST') { + res.writeHead(405, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Method not allowed' })) + return + } + + if (!STRIPE_SECRET_KEY) { + res.writeHead(500, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: 'Stripe not configured' })) + return + } + + let body = '' + req.on('data', chunk => body += chunk) + req.on('end', async () => { + try { + const { price_id, mode, customer_email } = JSON.parse(body) + const result = await createCheckoutSession(price_id, mode, customer_email) + + if (result.status === 200) { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ url: result.body.url, session_id: result.body.id })) + } else { + res.writeHead(result.status, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: result.body.error?.message || 'Stripe error' })) + } + } catch (e) { + res.writeHead(400, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ error: e.message })) + } + }) +}) + +server.listen(PORT, () => { + console.log(`Checkout API running on :${PORT}`) +}) diff --git a/site/checkout.md b/site/checkout.md index a398d52..b1b7516 100644 --- a/site/checkout.md +++ b/site/checkout.md @@ -12,9 +12,9 @@ const { site } = useData() const router = useRouter() const PRICES = { - 'self-paced': { price_id: 'price_self_paced', name: 'Self-Paced', amount: 9700 }, - 'enterprise': { price_id: 'price_enterprise', name: 'Enterprise', amount: 19900 }, - 'cohort': { price_id: 'price_cohort', name: 'Cohort', amount: 24700 }, + 'self-paced': { price_id: 'price_1TeZdgQGFaSjzUrJE4aoSlPb', name: 'Self-Paced', amount: 9700 }, + 'enterprise': { price_id: 'price_1TeZdhQGFaSjzUrJxVhrpvQj', name: 'Enterprise', amount: 19900 }, + 'cohort': { price_id: 'price_1TeZdiQGFaSjzUrJpoX01VPn', name: 'Cohort', amount: 24700 }, } onMounted(async () => {