/ scripts / setup-stripe.ts
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();