stripe-no-webhooks 0.0.3 ā 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +531 -51
- package/dist/BillingConfig-BYrAQ7Wx.d.mts +18 -0
- package/dist/BillingConfig-BYrAQ7Wx.d.ts +18 -0
- package/dist/client.d.mts +79 -0
- package/dist/client.d.ts +79 -0
- package/dist/client.js +55 -0
- package/dist/client.mjs +29 -0
- package/dist/index.d.mts +72 -9
- package/dist/index.d.ts +72 -9
- package/dist/index.js +137 -11
- package/dist/index.mjs +136 -10
- package/package.json +13 -6
- package/src/templates/app-router.ts +20 -0
- package/src/templates/billing.config.ts +26 -0
- package/src/templates/pages-router.ts +49 -0
package/bin/cli.js
CHANGED
|
@@ -5,6 +5,10 @@ const readline = require("readline");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
|
|
8
|
+
// Load environment variables from .env files in the user's project directory
|
|
9
|
+
require("dotenv").config({ path: path.join(process.cwd(), ".env.local") });
|
|
10
|
+
require("dotenv").config({ path: path.join(process.cwd(), ".env") });
|
|
11
|
+
|
|
8
12
|
const args = process.argv.slice(2);
|
|
9
13
|
const command = args[0];
|
|
10
14
|
const databaseUrl = args[1];
|
|
@@ -25,12 +29,21 @@ function question(rl, query, defaultValue = "") {
|
|
|
25
29
|
});
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
function
|
|
32
|
+
function maskSecretKey(key) {
|
|
33
|
+
if (!key || key.length < 8) return "*****";
|
|
34
|
+
return key.slice(0, 3) + "*****" + key.slice(-4);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function questionHidden(rl, query, defaultValue = "") {
|
|
29
38
|
return new Promise((resolve) => {
|
|
30
39
|
const stdin = process.stdin;
|
|
31
40
|
const stdout = process.stdout;
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
const maskedDefault = defaultValue ? maskSecretKey(defaultValue) : "";
|
|
43
|
+
const prompt = maskedDefault
|
|
44
|
+
? `${query} (${maskedDefault}): `
|
|
45
|
+
: `${query}: `;
|
|
46
|
+
stdout.write(prompt);
|
|
34
47
|
|
|
35
48
|
stdin.setRawMode(true);
|
|
36
49
|
stdin.resume();
|
|
@@ -43,7 +56,7 @@ function questionHidden(rl, query) {
|
|
|
43
56
|
stdin.pause();
|
|
44
57
|
stdin.removeListener("data", onData);
|
|
45
58
|
stdout.write("\n");
|
|
46
|
-
resolve(input);
|
|
59
|
+
resolve(input || defaultValue);
|
|
47
60
|
} else if (char === "\u0003") {
|
|
48
61
|
// Ctrl+C
|
|
49
62
|
process.exit();
|
|
@@ -55,7 +68,7 @@ function questionHidden(rl, query) {
|
|
|
55
68
|
}
|
|
56
69
|
} else {
|
|
57
70
|
input += char;
|
|
58
|
-
stdout.write("
|
|
71
|
+
stdout.write("*"); // Show asterisk for each character
|
|
59
72
|
}
|
|
60
73
|
};
|
|
61
74
|
|
|
@@ -80,6 +93,13 @@ async function migrate(databaseUrl) {
|
|
|
80
93
|
logger: console,
|
|
81
94
|
});
|
|
82
95
|
console.log("ā
Migrations completed successfully!");
|
|
96
|
+
|
|
97
|
+
// Save DATABASE_URL to env files
|
|
98
|
+
const envVars = [{ key: "DATABASE_URL", value: databaseUrl }];
|
|
99
|
+
const updatedFiles = saveToEnvFiles(envVars);
|
|
100
|
+
if (updatedFiles.length > 0) {
|
|
101
|
+
console.log(`š Updated ${updatedFiles.join(", ")} with DATABASE_URL`);
|
|
102
|
+
}
|
|
83
103
|
} catch (error) {
|
|
84
104
|
console.error("ā Migration failed:");
|
|
85
105
|
console.error(error);
|
|
@@ -87,6 +107,126 @@ async function migrate(databaseUrl) {
|
|
|
87
107
|
}
|
|
88
108
|
}
|
|
89
109
|
|
|
110
|
+
function saveToEnvFiles(envVars) {
|
|
111
|
+
const envFiles = [
|
|
112
|
+
".env",
|
|
113
|
+
".env.local",
|
|
114
|
+
".env.development",
|
|
115
|
+
".env.production",
|
|
116
|
+
];
|
|
117
|
+
const cwd = process.cwd();
|
|
118
|
+
const updatedFiles = [];
|
|
119
|
+
|
|
120
|
+
for (const envFile of envFiles) {
|
|
121
|
+
const envPath = path.join(cwd, envFile);
|
|
122
|
+
if (fs.existsSync(envPath)) {
|
|
123
|
+
let content = fs.readFileSync(envPath, "utf8");
|
|
124
|
+
|
|
125
|
+
for (const { key, value } of envVars) {
|
|
126
|
+
const line = `${key}=${value}`;
|
|
127
|
+
const regex = new RegExp(`^${key}=.*`, "m");
|
|
128
|
+
|
|
129
|
+
if (regex.test(content)) {
|
|
130
|
+
// Replace existing value
|
|
131
|
+
content = content.replace(regex, line);
|
|
132
|
+
} else {
|
|
133
|
+
// Append to file
|
|
134
|
+
const newline = content.endsWith("\n") ? "" : "\n";
|
|
135
|
+
content = content + newline + line + "\n";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fs.writeFileSync(envPath, content);
|
|
140
|
+
updatedFiles.push(envFile);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return updatedFiles;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getTemplatesDir() {
|
|
148
|
+
return path.join(__dirname, "..", "src", "templates");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getAppRouterTemplate() {
|
|
152
|
+
const templatePath = path.join(getTemplatesDir(), "app-router.ts");
|
|
153
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getPagesRouterTemplate() {
|
|
157
|
+
const templatePath = path.join(getTemplatesDir(), "pages-router.ts");
|
|
158
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function detectRouterType() {
|
|
162
|
+
const cwd = process.cwd();
|
|
163
|
+
const hasAppDir = fs.existsSync(path.join(cwd, "app"));
|
|
164
|
+
const hasPagesDir = fs.existsSync(path.join(cwd, "pages"));
|
|
165
|
+
|
|
166
|
+
// Also check for src/app and src/pages (common Next.js structure)
|
|
167
|
+
const hasSrcAppDir = fs.existsSync(path.join(cwd, "src", "app"));
|
|
168
|
+
const hasSrcPagesDir = fs.existsSync(path.join(cwd, "src", "pages"));
|
|
169
|
+
|
|
170
|
+
// Prefer App Router if app directory exists
|
|
171
|
+
if (hasAppDir || hasSrcAppDir) {
|
|
172
|
+
return { type: "app", useSrc: hasSrcAppDir && !hasAppDir };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (hasPagesDir || hasSrcPagesDir) {
|
|
176
|
+
return { type: "pages", useSrc: hasSrcPagesDir && !hasPagesDir };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Default to App Router if no directories found
|
|
180
|
+
return { type: "app", useSrc: false };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createApiRoute(routerType, useSrc) {
|
|
184
|
+
const cwd = process.cwd();
|
|
185
|
+
const baseDir = useSrc ? path.join(cwd, "src") : cwd;
|
|
186
|
+
|
|
187
|
+
if (routerType === "app") {
|
|
188
|
+
// App Router: app/api/stripe/[...all]/route.ts
|
|
189
|
+
const routeDir = path.join(baseDir, "app", "api", "stripe", "[...all]");
|
|
190
|
+
const routeFile = path.join(routeDir, "route.ts");
|
|
191
|
+
|
|
192
|
+
// Create directories if they don't exist
|
|
193
|
+
fs.mkdirSync(routeDir, { recursive: true });
|
|
194
|
+
|
|
195
|
+
// Get template content (remove the comment with file path)
|
|
196
|
+
let template = getAppRouterTemplate();
|
|
197
|
+
template = template.replace(
|
|
198
|
+
/^\/\/ app\/api\/stripe\/\[\.\.\.all\]\/route\.ts\n/,
|
|
199
|
+
""
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Write the file
|
|
203
|
+
fs.writeFileSync(routeFile, template);
|
|
204
|
+
|
|
205
|
+
const prefix = useSrc ? "src/" : "";
|
|
206
|
+
return `${prefix}app/api/stripe/[...all]/route.ts`;
|
|
207
|
+
} else {
|
|
208
|
+
// Pages Router: pages/api/stripe/[...all].ts
|
|
209
|
+
const routeDir = path.join(baseDir, "pages", "api", "stripe");
|
|
210
|
+
const routeFile = path.join(routeDir, "[...all].ts");
|
|
211
|
+
|
|
212
|
+
// Create directories if they don't exist
|
|
213
|
+
fs.mkdirSync(routeDir, { recursive: true });
|
|
214
|
+
|
|
215
|
+
// Get template content (remove the comment with file path)
|
|
216
|
+
let template = getPagesRouterTemplate();
|
|
217
|
+
template = template.replace(
|
|
218
|
+
/^\/\/ pages\/api\/stripe\/\[\.\.\.all\]\.ts\n/,
|
|
219
|
+
""
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Write the file
|
|
223
|
+
fs.writeFileSync(routeFile, template);
|
|
224
|
+
|
|
225
|
+
const prefix = useSrc ? "src/" : "";
|
|
226
|
+
return `${prefix}pages/api/stripe/[...all].ts`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
90
230
|
async function config() {
|
|
91
231
|
let Stripe;
|
|
92
232
|
try {
|
|
@@ -97,15 +237,20 @@ async function config() {
|
|
|
97
237
|
process.exit(1);
|
|
98
238
|
}
|
|
99
239
|
|
|
100
|
-
const rl = createPrompt();
|
|
101
|
-
|
|
102
240
|
console.log("\nš§ Stripe Webhook Configuration\n");
|
|
103
241
|
|
|
242
|
+
// Detect router type from folder structure
|
|
243
|
+
const { type: routerType, useSrc } = detectRouterType();
|
|
244
|
+
const routerLabel = routerType === "app" ? "App Router" : "Pages Router";
|
|
245
|
+
const srcLabel = useSrc ? " (src/)" : "";
|
|
246
|
+
console.log(`š Detected: ${routerLabel}${srcLabel}\n`);
|
|
247
|
+
|
|
104
248
|
// Get Stripe API key (hidden input)
|
|
105
|
-
|
|
249
|
+
const existingStripeKey = process.env.STRIPE_SECRET_KEY || "";
|
|
106
250
|
const stripeSecretKey = await questionHidden(
|
|
107
251
|
null,
|
|
108
|
-
"Enter your Stripe Secret Key (sk_...)"
|
|
252
|
+
"Enter your Stripe Secret Key (sk_...)",
|
|
253
|
+
existingStripeKey
|
|
109
254
|
);
|
|
110
255
|
|
|
111
256
|
if (!stripeSecretKey || !stripeSecretKey.startsWith("sk_")) {
|
|
@@ -113,16 +258,16 @@ async function config() {
|
|
|
113
258
|
process.exit(1);
|
|
114
259
|
}
|
|
115
260
|
|
|
116
|
-
//
|
|
117
|
-
const
|
|
261
|
+
// Create readline for site URL question
|
|
262
|
+
const rl = createPrompt();
|
|
118
263
|
|
|
119
264
|
// Get site URL with default from env
|
|
120
265
|
const defaultSiteUrl = process.env.NEXT_PUBLIC_SITE_URL || "";
|
|
121
|
-
const siteUrl = await question(
|
|
266
|
+
const siteUrl = await question(rl, "Enter your site URL", defaultSiteUrl);
|
|
122
267
|
|
|
123
268
|
if (!siteUrl) {
|
|
124
269
|
console.error("ā Site URL is required");
|
|
125
|
-
|
|
270
|
+
rl.close();
|
|
126
271
|
process.exit(1);
|
|
127
272
|
}
|
|
128
273
|
|
|
@@ -133,67 +278,105 @@ async function config() {
|
|
|
133
278
|
webhookUrl = `${url.origin}/api/stripe/webhook`;
|
|
134
279
|
} catch (e) {
|
|
135
280
|
console.error("ā Invalid URL format");
|
|
136
|
-
|
|
281
|
+
rl.close();
|
|
137
282
|
process.exit(1);
|
|
138
283
|
}
|
|
139
284
|
|
|
140
|
-
|
|
285
|
+
// Get DATABASE_URL (optional) - skip if already set in env
|
|
286
|
+
let databaseUrlInput = "";
|
|
287
|
+
if (process.env.DATABASE_URL) {
|
|
288
|
+
console.log("ā DATABASE_URL already set in environment");
|
|
289
|
+
databaseUrlInput = process.env.DATABASE_URL;
|
|
290
|
+
} else {
|
|
291
|
+
databaseUrlInput = await question(
|
|
292
|
+
rl,
|
|
293
|
+
"Enter your DATABASE_URL (optional, press Enter to skip)",
|
|
294
|
+
""
|
|
295
|
+
);
|
|
296
|
+
}
|
|
141
297
|
|
|
142
|
-
|
|
298
|
+
rl.close();
|
|
299
|
+
|
|
300
|
+
// Create the API route
|
|
301
|
+
console.log(`š Creating API route...`);
|
|
302
|
+
try {
|
|
303
|
+
const createdFile = createApiRoute(routerType, useSrc);
|
|
304
|
+
console.log(`ā
Created ${createdFile}`);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error("ā Failed to create API route:", error.message);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Copy billing.config.ts to root
|
|
311
|
+
console.log(`š Creating billing.config.ts...`);
|
|
312
|
+
try {
|
|
313
|
+
const billingConfigPath = path.join(process.cwd(), "billing.config.ts");
|
|
314
|
+
if (!fs.existsSync(billingConfigPath)) {
|
|
315
|
+
const templatePath = path.join(getTemplatesDir(), "billing.config.ts");
|
|
316
|
+
const template = fs.readFileSync(templatePath, "utf8");
|
|
317
|
+
fs.writeFileSync(billingConfigPath, template);
|
|
318
|
+
console.log(`ā
Created billing.config.ts\n`);
|
|
319
|
+
} else {
|
|
320
|
+
console.log(`ā billing.config.ts already exists\n`);
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error("ā Failed to create billing.config.ts:", error.message);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log(`š” Creating webhook endpoint: ${webhookUrl}\n`);
|
|
143
328
|
|
|
144
329
|
const stripe = new Stripe(stripeSecretKey);
|
|
145
330
|
|
|
146
331
|
try {
|
|
147
|
-
//
|
|
332
|
+
// Check if a webhook with the same URL already exists
|
|
333
|
+
const existingWebhooks = await stripe.webhookEndpoints.list({ limit: 100 });
|
|
334
|
+
const existingWebhook = existingWebhooks.data.find(
|
|
335
|
+
(wh) => wh.url === webhookUrl
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (existingWebhook) {
|
|
339
|
+
console.log(`š Found existing webhook with same URL, deleting it...`);
|
|
340
|
+
await stripe.webhookEndpoints.del(existingWebhook.id);
|
|
341
|
+
console.log(`ā
Deleted existing webhook (${existingWebhook.id})\n`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Create webhook endpoint
|
|
345
|
+
console.log(`š Creating new webhook endpoint...`);
|
|
148
346
|
const webhook = await stripe.webhookEndpoints.create({
|
|
149
347
|
url: webhookUrl,
|
|
150
348
|
enabled_events: ["*"], // Listen to all events
|
|
151
349
|
description: "Created by stripe-no-webhooks CLI",
|
|
152
350
|
});
|
|
153
|
-
|
|
154
351
|
console.log("ā
Webhook created successfully!\n");
|
|
155
352
|
|
|
156
|
-
//
|
|
157
|
-
const
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
".env.production",
|
|
353
|
+
// Build list of env vars to update
|
|
354
|
+
const envVars = [
|
|
355
|
+
{ key: "STRIPE_SECRET_KEY", value: stripeSecretKey },
|
|
356
|
+
{ key: "STRIPE_WEBHOOK_SECRET", value: webhook.secret },
|
|
357
|
+
{ key: "NEXT_PUBLIC_SITE_URL", value: siteUrl },
|
|
162
358
|
];
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (const envFile of envFiles) {
|
|
167
|
-
const envPath = path.join(cwd, envFile);
|
|
168
|
-
if (fs.existsSync(envPath)) {
|
|
169
|
-
let content = fs.readFileSync(envPath, "utf8");
|
|
170
|
-
const secretLine = `STRIPE_WEBHOOK_SECRET=${webhook.secret}`;
|
|
171
|
-
|
|
172
|
-
if (content.includes("STRIPE_WEBHOOK_SECRET=")) {
|
|
173
|
-
// Replace existing value
|
|
174
|
-
content = content.replace(/STRIPE_WEBHOOK_SECRET=.*/, secretLine);
|
|
175
|
-
} else {
|
|
176
|
-
// Append to file
|
|
177
|
-
const newline = content.endsWith("\n") ? "" : "\n";
|
|
178
|
-
content = content + newline + secretLine + "\n";
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
fs.writeFileSync(envPath, content);
|
|
182
|
-
updatedFiles.push(envFile);
|
|
183
|
-
}
|
|
359
|
+
if (databaseUrlInput) {
|
|
360
|
+
envVars.push({ key: "DATABASE_URL", value: databaseUrlInput });
|
|
184
361
|
}
|
|
185
362
|
|
|
363
|
+
// Save to env files
|
|
364
|
+
const updatedFiles = saveToEnvFiles(envVars);
|
|
365
|
+
|
|
366
|
+
const envVarNames = envVars.map((v) => v.key).join(", ");
|
|
186
367
|
if (updatedFiles.length > 0) {
|
|
368
|
+
console.log(`š Updated ${updatedFiles.join(", ")} with ${envVarNames}`);
|
|
187
369
|
console.log(
|
|
188
|
-
|
|
189
|
-
", "
|
|
190
|
-
)} with STRIPE_WEBHOOK_SECRET\nREMEMBER: Update the enviroment variable in Vercel too\nSTRIPE_WEBHOOK_SECRET=${
|
|
191
|
-
webhook.secret
|
|
192
|
-
}`
|
|
370
|
+
"\nREMEMBER: Update the environment variables in Vercel too:"
|
|
193
371
|
);
|
|
372
|
+
for (const { key, value } of envVars) {
|
|
373
|
+
console.log(`${key}=${value}`);
|
|
374
|
+
}
|
|
194
375
|
} else {
|
|
195
|
-
console.log("Add
|
|
196
|
-
|
|
376
|
+
console.log("Add these to your environment variables:\n");
|
|
377
|
+
for (const { key, value } of envVars) {
|
|
378
|
+
console.log(`${key}=${value}`);
|
|
379
|
+
}
|
|
197
380
|
}
|
|
198
381
|
|
|
199
382
|
console.log("ā".repeat(50));
|
|
@@ -217,6 +400,298 @@ async function config() {
|
|
|
217
400
|
}
|
|
218
401
|
}
|
|
219
402
|
|
|
403
|
+
function findMatchingBrace(content, startIndex) {
|
|
404
|
+
// Find the matching closing brace for an opening brace
|
|
405
|
+
let depth = 0;
|
|
406
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
407
|
+
if (content[i] === "{" || content[i] === "[") depth++;
|
|
408
|
+
else if (content[i] === "}" || content[i] === "]") {
|
|
409
|
+
depth--;
|
|
410
|
+
if (depth === 0) return i;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return -1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function parseBillingConfig(content) {
|
|
417
|
+
// Extract the plans array from the file content
|
|
418
|
+
const plansStartMatch = content.match(/plans\s*:\s*\[/);
|
|
419
|
+
if (!plansStartMatch) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const plansStart = plansStartMatch.index + plansStartMatch[0].length - 1;
|
|
424
|
+
const plansEnd = findMatchingBrace(content, plansStart);
|
|
425
|
+
if (plansEnd === -1) return [];
|
|
426
|
+
|
|
427
|
+
const plansContent = content.substring(plansStart + 1, plansEnd);
|
|
428
|
+
const plans = [];
|
|
429
|
+
|
|
430
|
+
// Find each plan object by looking for opening braces at the top level
|
|
431
|
+
let depth = 0;
|
|
432
|
+
let planStart = -1;
|
|
433
|
+
|
|
434
|
+
for (let i = 0; i < plansContent.length; i++) {
|
|
435
|
+
const char = plansContent[i];
|
|
436
|
+
if (char === "{") {
|
|
437
|
+
if (depth === 0) planStart = i;
|
|
438
|
+
depth++;
|
|
439
|
+
} else if (char === "}") {
|
|
440
|
+
depth--;
|
|
441
|
+
if (depth === 0 && planStart !== -1) {
|
|
442
|
+
const planRaw = plansContent.substring(planStart, i + 1);
|
|
443
|
+
const plan = parsePlanObject(planRaw);
|
|
444
|
+
if (plan.name) {
|
|
445
|
+
plans.push({
|
|
446
|
+
plan,
|
|
447
|
+
raw: planRaw,
|
|
448
|
+
startIndex:
|
|
449
|
+
plansStartMatch.index + plansStartMatch[0].length + planStart,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
planStart = -1;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return plans;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function parsePlanObject(planContent) {
|
|
461
|
+
const plan = {};
|
|
462
|
+
|
|
463
|
+
// Extract id if present (product id)
|
|
464
|
+
const idMatch = planContent.match(/^\s*\{\s*id\s*:\s*["']([^"']+)["']/);
|
|
465
|
+
if (idMatch) plan.id = idMatch[1];
|
|
466
|
+
|
|
467
|
+
// Also try to find id not at start
|
|
468
|
+
if (!plan.id) {
|
|
469
|
+
const idMatch2 = planContent.match(/[,{]\s*id\s*:\s*["']([^"']+)["']/);
|
|
470
|
+
if (idMatch2) plan.id = idMatch2[1];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Extract name
|
|
474
|
+
const nameMatch = planContent.match(/name\s*:\s*["']([^"']+)["']/);
|
|
475
|
+
if (nameMatch) plan.name = nameMatch[1];
|
|
476
|
+
|
|
477
|
+
// Extract description
|
|
478
|
+
const descMatch = planContent.match(/description\s*:\s*["']([^"']+)["']/);
|
|
479
|
+
if (descMatch) plan.description = descMatch[1];
|
|
480
|
+
|
|
481
|
+
// Extract price array
|
|
482
|
+
const priceStartMatch = planContent.match(/price\s*:\s*\[/);
|
|
483
|
+
if (priceStartMatch) {
|
|
484
|
+
const priceStart = priceStartMatch.index + priceStartMatch[0].length - 1;
|
|
485
|
+
const priceEnd = findMatchingBrace(planContent, priceStart);
|
|
486
|
+
if (priceEnd !== -1) {
|
|
487
|
+
const priceArrayContent = planContent.substring(priceStart + 1, priceEnd);
|
|
488
|
+
plan.prices = parsePriceArray(priceArrayContent);
|
|
489
|
+
plan.priceArrayStart = priceStart;
|
|
490
|
+
plan.priceArrayEnd = priceEnd;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return plan;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function parsePriceArray(priceArrayContent) {
|
|
498
|
+
const prices = [];
|
|
499
|
+
let depth = 0;
|
|
500
|
+
let priceStart = -1;
|
|
501
|
+
|
|
502
|
+
for (let i = 0; i < priceArrayContent.length; i++) {
|
|
503
|
+
const char = priceArrayContent[i];
|
|
504
|
+
if (char === "{") {
|
|
505
|
+
if (depth === 0) priceStart = i;
|
|
506
|
+
depth++;
|
|
507
|
+
} else if (char === "}") {
|
|
508
|
+
depth--;
|
|
509
|
+
if (depth === 0 && priceStart !== -1) {
|
|
510
|
+
const priceRaw = priceArrayContent.substring(priceStart, i + 1);
|
|
511
|
+
const price = parsePriceObject(priceRaw);
|
|
512
|
+
prices.push({ price, raw: priceRaw, localStart: priceStart });
|
|
513
|
+
priceStart = -1;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return prices;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function parsePriceObject(priceContent) {
|
|
522
|
+
const price = {};
|
|
523
|
+
|
|
524
|
+
// Extract id if present (price id)
|
|
525
|
+
const idMatch = priceContent.match(/id\s*:\s*["']([^"']+)["']/);
|
|
526
|
+
if (idMatch) price.id = idMatch[1];
|
|
527
|
+
|
|
528
|
+
// Extract amount
|
|
529
|
+
const amountMatch = priceContent.match(/amount\s*:\s*(\d+)/);
|
|
530
|
+
if (amountMatch) price.amount = parseInt(amountMatch[1], 10);
|
|
531
|
+
|
|
532
|
+
// Extract currency
|
|
533
|
+
const currencyMatch = priceContent.match(/currency\s*:\s*["']([^"']+)["']/);
|
|
534
|
+
if (currencyMatch) price.currency = currencyMatch[1];
|
|
535
|
+
|
|
536
|
+
// Extract interval
|
|
537
|
+
const intervalMatch = priceContent.match(/interval\s*:\s*["']([^"']+)["']/);
|
|
538
|
+
if (intervalMatch) price.interval = intervalMatch[1];
|
|
539
|
+
|
|
540
|
+
return price;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function sync() {
|
|
544
|
+
const billingConfigPath = path.join(process.cwd(), "billing.config.ts");
|
|
545
|
+
|
|
546
|
+
if (!fs.existsSync(billingConfigPath)) {
|
|
547
|
+
console.error("ā billing.config.ts not found in project root.");
|
|
548
|
+
console.log("Run 'npx stripe-no-webhooks config' first to create it.");
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let Stripe;
|
|
553
|
+
try {
|
|
554
|
+
Stripe = require("stripe").default || require("stripe");
|
|
555
|
+
} catch (e) {
|
|
556
|
+
console.error("ā Stripe package not found.");
|
|
557
|
+
console.log("Please install it first: npm install stripe");
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Get Stripe API key from env or prompt
|
|
562
|
+
let stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
|
563
|
+
if (!stripeSecretKey) {
|
|
564
|
+
stripeSecretKey = await questionHidden(
|
|
565
|
+
null,
|
|
566
|
+
"Enter your Stripe Secret Key (sk_...)"
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (!stripeSecretKey || !stripeSecretKey.startsWith("sk_")) {
|
|
571
|
+
console.error("ā Invalid Stripe Secret Key. It should start with 'sk_'");
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const stripe = new Stripe(stripeSecretKey);
|
|
576
|
+
|
|
577
|
+
console.log("\nš¤ Pushing billing plans to Stripe...\n");
|
|
578
|
+
|
|
579
|
+
let content = fs.readFileSync(billingConfigPath, "utf8");
|
|
580
|
+
const parsedPlans = parseBillingConfig(content);
|
|
581
|
+
|
|
582
|
+
if (parsedPlans.length === 0) {
|
|
583
|
+
console.log("No plans found in billing.config.ts");
|
|
584
|
+
console.log("Add plans to the config file and run this command again.");
|
|
585
|
+
process.exit(0);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let updatedContent = content;
|
|
589
|
+
let productsCreated = 0;
|
|
590
|
+
let pricesCreated = 0;
|
|
591
|
+
let skippedProducts = 0;
|
|
592
|
+
let skippedPrices = 0;
|
|
593
|
+
|
|
594
|
+
for (const { plan, raw } of parsedPlans) {
|
|
595
|
+
let productId = plan.id;
|
|
596
|
+
let updatedPlanRaw = raw;
|
|
597
|
+
|
|
598
|
+
// Create product if needed
|
|
599
|
+
if (!productId) {
|
|
600
|
+
try {
|
|
601
|
+
console.log(`š Creating product "${plan.name}" in Stripe...`);
|
|
602
|
+
|
|
603
|
+
const product = await stripe.products.create({
|
|
604
|
+
name: plan.name,
|
|
605
|
+
description: plan.description || undefined,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
productId = product.id;
|
|
609
|
+
console.log(`ā
Created product "${plan.name}" (${productId})`);
|
|
610
|
+
|
|
611
|
+
// Add product id to plan
|
|
612
|
+
updatedPlanRaw = updatedPlanRaw.replace(
|
|
613
|
+
/\{/,
|
|
614
|
+
`{\n id: "${productId}",`
|
|
615
|
+
);
|
|
616
|
+
productsCreated++;
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error(
|
|
619
|
+
`ā Failed to create product "${plan.name}":`,
|
|
620
|
+
error.message
|
|
621
|
+
);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
console.log(`āļø Product "${plan.name}" already exists (${productId})`);
|
|
626
|
+
skippedProducts++;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Create prices if needed
|
|
630
|
+
if (plan.prices && plan.prices.length > 0) {
|
|
631
|
+
for (const { price, raw: priceRaw } of plan.prices) {
|
|
632
|
+
if (price.id) {
|
|
633
|
+
console.log(
|
|
634
|
+
` āļø Price ${price.interval}/${price.currency} already exists (${price.id})`
|
|
635
|
+
);
|
|
636
|
+
skippedPrices++;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
console.log(
|
|
642
|
+
` š Creating price ${price.amount / 100} ${price.currency}/${
|
|
643
|
+
price.interval
|
|
644
|
+
}...`
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const stripePrice = await stripe.prices.create({
|
|
648
|
+
product: productId,
|
|
649
|
+
unit_amount: price.amount,
|
|
650
|
+
currency: price.currency.toLowerCase(),
|
|
651
|
+
recurring: {
|
|
652
|
+
interval: price.interval,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
console.log(` ā
Created price (${stripePrice.id})`);
|
|
657
|
+
|
|
658
|
+
// Add price id to price object
|
|
659
|
+
const updatedPriceRaw = priceRaw.replace(
|
|
660
|
+
/\{/,
|
|
661
|
+
`{\n id: "${stripePrice.id}",`
|
|
662
|
+
);
|
|
663
|
+
updatedPlanRaw = updatedPlanRaw.replace(priceRaw, updatedPriceRaw);
|
|
664
|
+
pricesCreated++;
|
|
665
|
+
} catch (error) {
|
|
666
|
+
console.error(
|
|
667
|
+
` ā Failed to create price ${price.interval}/${price.currency}:`,
|
|
668
|
+
error.message
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Update content with modified plan
|
|
675
|
+
if (updatedPlanRaw !== raw) {
|
|
676
|
+
updatedContent = updatedContent.replace(raw, updatedPlanRaw);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Write updated content back to file
|
|
681
|
+
if (productsCreated > 0 || pricesCreated > 0) {
|
|
682
|
+
fs.writeFileSync(billingConfigPath, updatedContent);
|
|
683
|
+
console.log(
|
|
684
|
+
`\nš Updated billing.config.ts with ${productsCreated} product(s) and ${pricesCreated} price(s)`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
console.log(`\nā
Done!`);
|
|
689
|
+
console.log(
|
|
690
|
+
` Products: ${productsCreated} created, ${skippedProducts} skipped`
|
|
691
|
+
);
|
|
692
|
+
console.log(` Prices: ${pricesCreated} created, ${skippedPrices} skipped`);
|
|
693
|
+
}
|
|
694
|
+
|
|
220
695
|
async function main() {
|
|
221
696
|
switch (command) {
|
|
222
697
|
case "migrate":
|
|
@@ -227,10 +702,15 @@ async function main() {
|
|
|
227
702
|
await config();
|
|
228
703
|
break;
|
|
229
704
|
|
|
705
|
+
case "sync":
|
|
706
|
+
await sync();
|
|
707
|
+
break;
|
|
708
|
+
|
|
230
709
|
default:
|
|
231
710
|
console.log("Usage:");
|
|
232
711
|
console.log(" npx stripe-no-webhooks migrate <connection_string>");
|
|
233
712
|
console.log(" npx stripe-no-webhooks config");
|
|
713
|
+
console.log(" npx stripe-no-webhooks push");
|
|
234
714
|
process.exit(1);
|
|
235
715
|
}
|
|
236
716
|
}
|