setup-stripe.ts
1 #!/usr/bin/env -S node 2 3 import * as dotenv from "dotenv"; 4 import * as path from "path"; 5 import Stripe from "stripe"; 6 7 dotenv.config({ path: path.join(process.cwd(), ".env"), quiet: true }); 8 9 const stripe = new Stripe(process.env.STRIPE_SECRET_SERVER_SIDE_KEY || "", { 10 apiVersion: "2026-01-28.clover", 11 }); 12 13 const PRODUCT_NAME = "Radicle Garden"; 14 15 interface SetupResult { 16 productId: string; 17 priceId: string; 18 webhookEndpointId: string; 19 webhookSecret: string; 20 } 21 22 async function setupStripe(): Promise<SetupResult> { 23 console.log("š Setting up Stripe...\n"); 24 25 const existingProducts = await stripe.products.search({ 26 query: `name:"${PRODUCT_NAME}"`, 27 }); 28 29 let product: Stripe.Product; 30 if (existingProducts.data.length > 0) { 31 product = existingProducts.data[0]; 32 console.log(`ā Found existing product: ${product.id}`); 33 } else { 34 product = await stripe.products.create({ 35 name: PRODUCT_NAME, 36 description: `Monthly subscription for ${PRODUCT_NAME} with 7-day trial`, 37 metadata: { 38 environment: process.env.NODE_ENV || "development", 39 }, 40 }); 41 console.log(`ā Created product: ${product.id}`); 42 } 43 44 const existingPrices = await stripe.prices.list({ 45 product: product.id, 46 active: true, 47 }); 48 49 let price: Stripe.Price; 50 if (existingPrices.data.length > 0) { 51 price = existingPrices.data[0]; 52 console.log(`ā Found existing price: ${price.id}`); 53 } else { 54 price = await stripe.prices.create({ 55 product: product.id, 56 currency: "eur", 57 recurring: { 58 interval: "month", 59 trial_period_days: 7, 60 }, 61 unit_amount: 499, // 4.99 EUR in cents. 62 metadata: { 63 environment: process.env.NODE_ENV || "development", 64 }, 65 }); 66 console.log(`ā Created price: ${price.id} (ā¬4.99/month with 7-day trial)`); 67 } 68 69 const webhookUrl = 70 process.env.WEBHOOK_URL || "http://localhost:5173/api/stripe/webhook"; 71 const isLocalhost = 72 webhookUrl.includes("localhost") || webhookUrl.includes("127.0.0.1"); 73 74 let webhook: Stripe.WebhookEndpoint | null = null; 75 let webhookSecret = ""; 76 77 if (isLocalhost) { 78 console.log( 79 "š Skipping webhook registration (localhost URL detected) š for local development, use Stripe CLI to forward webhooks", 80 ); 81 } else { 82 const existingWebhooks = await stripe.webhookEndpoints.list(); 83 const matchingWebhook = existingWebhooks.data.find( 84 wh => wh.url === webhookUrl, 85 ); 86 87 if (matchingWebhook) { 88 webhook = matchingWebhook; 89 webhookSecret = webhook.secret || ""; 90 console.log(`ā Found existing webhook: ${webhook.id}`); 91 } else { 92 webhook = await stripe.webhookEndpoints.create({ 93 url: webhookUrl, 94 enabled_events: [ 95 "customer.subscription.created", 96 "customer.subscription.updated", 97 "customer.subscription.deleted", 98 "invoice.payment_failed", 99 "customer.subscription.trial_will_end", 100 ], 101 api_version: "2026-01-28.clover", 102 }); 103 webhookSecret = webhook.secret || ""; 104 console.log(`ā Created webhook: ${webhook.id}`); 105 } 106 } 107 108 try { 109 const portalConfigs = await stripe.billingPortal.configurations.list({ 110 limit: 1, 111 }); 112 113 if (portalConfigs.data.length > 0) { 114 const config = portalConfigs.data[0]; 115 await stripe.billingPortal.configurations.update(config.id, { 116 features: { 117 payment_method_update: { 118 enabled: true, 119 }, 120 subscription_cancel: { 121 enabled: true, 122 mode: "at_period_end", 123 }, 124 invoice_history: { 125 enabled: true, 126 }, 127 }, 128 }); 129 console.log(`ā Updated Customer Portal configuration: ${config.id}`); 130 } else { 131 const newConfig = await stripe.billingPortal.configurations.create({ 132 features: { 133 payment_method_update: { 134 enabled: true, 135 }, 136 subscription_cancel: { 137 enabled: true, 138 mode: "at_period_end", 139 }, 140 invoice_history: { 141 enabled: true, 142 }, 143 }, 144 business_profile: { 145 headline: "Manage your Always-On Node subscription", 146 }, 147 }); 148 console.log(`ā Created Customer Portal configuration: ${newConfig.id}`); 149 } 150 } catch (error) { 151 console.warn("ā ļø Failed to configure Customer Portal:", error); 152 console.log( 153 " You may need to configure the Customer Portal manually in the Stripe Dashboard", 154 ); 155 } 156 157 return { 158 productId: product.id, 159 priceId: price.id, 160 webhookEndpointId: webhook?.id || "", 161 webhookSecret: webhookSecret, 162 }; 163 } 164 165 async function main() { 166 try { 167 if (!process.env.STRIPE_SECRET_SERVER_SIDE_KEY) { 168 throw new Error("STRIPE_SECRET_SERVER_SIDE_KEY is not set in .env"); 169 } 170 171 const result = await setupStripe(); 172 173 console.log("\nš Setup complete!\n"); 174 console.log("Configuration:\n"); 175 console.log(` Product ID: ${result.productId}`); 176 console.log(` Price ID: ${result.priceId}`); 177 if (result.webhookEndpointId) { 178 console.log(` Webhook ID: ${result.webhookEndpointId}`); 179 } 180 181 console.log("\nš Add to your .env file:\n"); 182 console.log(` STRIPE_PRICE_ID=${result.priceId}`); 183 if (result.webhookSecret) { 184 console.log(` STRIPE_WEBHOOK_SECRET=${result.webhookSecret}`); 185 } 186 187 if (!result.webhookEndpointId) { 188 console.log("\nš” For local development:"); 189 console.log( 190 " Run: stripe listen --forward-to localhost:5173/api/stripe/webhook", 191 ); 192 console.log( 193 " Copy the webhook signing secret to STRIPE_WEBHOOK_SECRET in .env", 194 ); 195 console.log("\nš” For PRODUCTION deployment:"); 196 console.log( 197 " Set WEBHOOK_URL to your public domain and re-run this script:", 198 ); 199 console.log( 200 " WEBHOOK_URL=https://your-domain.com/api/stripe/webhook pnpm setup:stripe\n", 201 ); 202 } 203 } catch (error) { 204 console.error("ā Setup failed:", error); 205 process.exit(1); 206 } 207 } 208 209 main();