stripe-no-webhooks 0.0.8 → 0.0.9
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/README.md +64 -126
- package/bin/cli.js +25 -45
- package/dist/index.d.mts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +117 -6
- package/dist/index.mjs +117 -6
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,152 +1,90 @@
|
|
|
1
1
|
# stripe-no-webhooks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Opinionated & Open Source library that automatically syncs Stripe to your database and gives you useful helpers to implement subscriptions.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why this library?
|
|
6
|
+
|
|
7
|
+
Stripe documentation lacks the ability to clearly point you to an easy way to implement Stripe. Depending on what you google you might end up in a weird place and shoot yourself in the foot.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
### 1. Install
|
|
6
12
|
|
|
7
13
|
```bash
|
|
8
14
|
npm install stripe-no-webhooks stripe
|
|
9
15
|
```
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
### 1. Create Stripe schema and tables
|
|
17
|
+
Note: make sure you also have `.env` or `.env.local` in your project so it can save the generated secrets there.
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
### 2. Create tables where all Stripe data will be automatically synced
|
|
16
20
|
|
|
17
21
|
```bash
|
|
18
22
|
npx stripe-no-webhooks migrate postgresql://postgres.[USER]:[PASSWORD]@[DB_URL]/postgres
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
### 2. Set up the webhook handler
|
|
24
|
-
|
|
25
|
-
Create a webhook endpoint in your Next.js app:
|
|
26
|
-
|
|
27
|
-
#### App Router (recommended)
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
// app/api/stripe/webhook/route.ts
|
|
31
|
-
import { createStripeWebhookHandler } from "stripe-no-webhooks";
|
|
32
|
-
|
|
33
|
-
const handler = createStripeWebhookHandler({
|
|
34
|
-
databaseUrl: process.env.DATABASE_URL!,
|
|
35
|
-
stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
|
|
36
|
-
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
37
|
-
callbacks: {
|
|
38
|
-
onSubscriptionCreated: async (subscription) => {
|
|
39
|
-
// Called when a new subscription is created
|
|
40
|
-
console.log("New subscription:", subscription.id);
|
|
41
|
-
// e.g., send welcome email, provision resources, etc.
|
|
42
|
-
},
|
|
43
|
-
onSubscriptionCancelled: async (subscription) => {
|
|
44
|
-
// Called when a subscription is cancelled
|
|
45
|
-
console.log("Subscription cancelled:", subscription.id);
|
|
46
|
-
// e.g., send cancellation email, revoke access, etc.
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
});
|
|
25
|
+
### 3. Run `config` to generate files & webhook
|
|
50
26
|
|
|
51
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npx stripe-no-webhooks config
|
|
52
29
|
```
|
|
53
30
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
//
|
|
58
|
-
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
31
|
+
### 4. Create your plans
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
// billing.config.ts (automatically created during config)
|
|
35
|
+
import type { BillingConfig } from "stripe-no-webhooks";
|
|
36
|
+
const billingConfig: BillingConfig = {
|
|
37
|
+
test: {
|
|
38
|
+
plans: [
|
|
39
|
+
{
|
|
40
|
+
name: "Premium",
|
|
41
|
+
description: "Access to all features",
|
|
42
|
+
price: [
|
|
43
|
+
{
|
|
44
|
+
amount: 1000, // $10
|
|
45
|
+
currency: "usd",
|
|
46
|
+
interval: "month",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
amount: 10000, // $100
|
|
50
|
+
currency: "usd",
|
|
51
|
+
interval: "year",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
79
56
|
},
|
|
80
57
|
};
|
|
81
|
-
|
|
82
|
-
export default async function webhookHandler(
|
|
83
|
-
req: NextApiRequest,
|
|
84
|
-
res: NextApiResponse
|
|
85
|
-
) {
|
|
86
|
-
if (req.method !== "POST") {
|
|
87
|
-
return res.status(405).json({ error: "Method not allowed" });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Convert NextApiRequest to Request for the handler
|
|
91
|
-
const body = await new Promise<string>((resolve) => {
|
|
92
|
-
let data = "";
|
|
93
|
-
req.on("data", (chunk) => (data += chunk));
|
|
94
|
-
req.on("end", () => resolve(data));
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const request = new Request(`https://${req.headers.host}${req.url}`, {
|
|
98
|
-
method: "POST",
|
|
99
|
-
headers: new Headers(req.headers as Record<string, string>),
|
|
100
|
-
body,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const response = await handler(request);
|
|
104
|
-
res.status(response.status).send(await response.text());
|
|
105
|
-
}
|
|
58
|
+
export default billingConfig;
|
|
106
59
|
```
|
|
107
60
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Run the config command to automatically create a webhook in your Stripe account:
|
|
61
|
+
Run sync:
|
|
111
62
|
|
|
112
63
|
```bash
|
|
113
|
-
npx stripe-no-webhooks
|
|
64
|
+
npx stripe-no-webhooks sync
|
|
114
65
|
```
|
|
115
66
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
67
|
+
### 5. Implement a checkout button in your frontend:
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
"use client";
|
|
71
|
+
import { checkout } from "stripe-no-webhooks/client";
|
|
72
|
+
|
|
73
|
+
export default function Home() {
|
|
74
|
+
return (
|
|
75
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
76
|
+
<button
|
|
77
|
+
className="bg-blue-500 text-white px-4 py-2 rounded-md cursor-pointer"
|
|
78
|
+
onClick={() =>
|
|
79
|
+
checkout({
|
|
80
|
+
planName: "Premium",
|
|
81
|
+
interval: "month",
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
Checkout
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
129
90
|
```
|
|
130
|
-
|
|
131
|
-
## What gets synced?
|
|
132
|
-
|
|
133
|
-
All Stripe webhook events are automatically synced to your PostgreSQL database in the `stripe` schema. This includes:
|
|
134
|
-
|
|
135
|
-
- Customers
|
|
136
|
-
- Subscriptions
|
|
137
|
-
- Products
|
|
138
|
-
- Prices
|
|
139
|
-
- Invoices
|
|
140
|
-
- Payment methods
|
|
141
|
-
- And more...
|
|
142
|
-
|
|
143
|
-
You can query this data directly from your database without making API calls to Stripe.
|
|
144
|
-
|
|
145
|
-
## Callbacks
|
|
146
|
-
|
|
147
|
-
| Callback | Event | Description |
|
|
148
|
-
| ------------------------- | --------------------------------------------------------------- | ----------------------------------------- |
|
|
149
|
-
| `onSubscriptionCreated` | `customer.subscription.created` | Called when a new subscription is created |
|
|
150
|
-
| `onSubscriptionCancelled` | `customer.subscription.deleted` or status changes to `canceled` | Called when a subscription is cancelled |
|
|
151
|
-
|
|
152
|
-
Both callbacks receive the full Stripe `Subscription` object.
|
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ const { runMigrations } = require("@supabase/stripe-sync-engine");
|
|
|
4
4
|
const readline = require("readline");
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
|
+
const { Client } = require("pg");
|
|
7
8
|
|
|
8
9
|
// Load environment variables from .env files in the user's project directory
|
|
9
10
|
require("dotenv").config({ path: path.join(process.cwd(), ".env.local") });
|
|
@@ -76,7 +77,9 @@ function questionHidden(rl, query, defaultValue = "") {
|
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
async function migrate(
|
|
80
|
+
async function migrate(dbUrl) {
|
|
81
|
+
const SCHEMA = "stripe";
|
|
82
|
+
const databaseUrl = dbUrl || process.env.DATABASE_URL;
|
|
80
83
|
if (!databaseUrl) {
|
|
81
84
|
console.error("❌ Missing database URL.\n");
|
|
82
85
|
console.log(
|
|
@@ -89,16 +92,30 @@ async function migrate(databaseUrl) {
|
|
|
89
92
|
try {
|
|
90
93
|
await runMigrations({
|
|
91
94
|
databaseUrl,
|
|
92
|
-
schema:
|
|
95
|
+
schema: SCHEMA,
|
|
93
96
|
logger: console,
|
|
94
97
|
});
|
|
95
|
-
|
|
98
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
99
|
+
await client.connect();
|
|
100
|
+
|
|
101
|
+
await client.query(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA}.user_stripe_customer_map (
|
|
103
|
+
user_id text PRIMARY KEY,
|
|
104
|
+
stripe_customer_id text UNIQUE NOT NULL,
|
|
105
|
+
created_at timestamptz DEFAULT now(),
|
|
106
|
+
updated_at timestamptz DEFAULT now()
|
|
107
|
+
);
|
|
108
|
+
`);
|
|
96
109
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
110
|
+
await client.end();
|
|
111
|
+
console.log("✅ Stripe schema migrations completed!");
|
|
112
|
+
|
|
113
|
+
if (!process.env.DATABASE_URL) {
|
|
114
|
+
const envVars = [{ key: "DATABASE_URL", value: databaseUrl }];
|
|
115
|
+
const updatedFiles = saveToEnvFiles(envVars);
|
|
116
|
+
if (updatedFiles.length > 0) {
|
|
117
|
+
console.log(`📝 Updated ${updatedFiles.join(", ")} with DATABASE_URL`);
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
120
|
} catch (error) {
|
|
104
121
|
console.error("❌ Migration failed:");
|
|
@@ -127,10 +144,8 @@ function saveToEnvFiles(envVars) {
|
|
|
127
144
|
const regex = new RegExp(`^${key}=.*`, "m");
|
|
128
145
|
|
|
129
146
|
if (regex.test(content)) {
|
|
130
|
-
// Replace existing value
|
|
131
147
|
content = content.replace(regex, line);
|
|
132
148
|
} else {
|
|
133
|
-
// Append to file
|
|
134
149
|
const newline = content.endsWith("\n") ? "" : "\n";
|
|
135
150
|
content = content + newline + line + "\n";
|
|
136
151
|
}
|
|
@@ -188,8 +203,6 @@ function createApiRoute(routerType, useSrc) {
|
|
|
188
203
|
// App Router: app/api/stripe/[...all]/route.ts
|
|
189
204
|
const routeDir = path.join(baseDir, "app", "api", "stripe", "[...all]");
|
|
190
205
|
const routeFile = path.join(routeDir, "route.ts");
|
|
191
|
-
|
|
192
|
-
// Create directories if they don't exist
|
|
193
206
|
fs.mkdirSync(routeDir, { recursive: true });
|
|
194
207
|
|
|
195
208
|
// Get template content (remove the comment with file path)
|
|
@@ -199,7 +212,6 @@ function createApiRoute(routerType, useSrc) {
|
|
|
199
212
|
""
|
|
200
213
|
);
|
|
201
214
|
|
|
202
|
-
// Write the file
|
|
203
215
|
fs.writeFileSync(routeFile, template);
|
|
204
216
|
|
|
205
217
|
const prefix = useSrc ? "src/" : "";
|
|
@@ -209,7 +221,6 @@ function createApiRoute(routerType, useSrc) {
|
|
|
209
221
|
const routeDir = path.join(baseDir, "pages", "api", "stripe");
|
|
210
222
|
const routeFile = path.join(routeDir, "[...all].ts");
|
|
211
223
|
|
|
212
|
-
// Create directories if they don't exist
|
|
213
224
|
fs.mkdirSync(routeDir, { recursive: true });
|
|
214
225
|
|
|
215
226
|
// Get template content (remove the comment with file path)
|
|
@@ -218,8 +229,6 @@ function createApiRoute(routerType, useSrc) {
|
|
|
218
229
|
/^\/\/ pages\/api\/stripe\/\[\.\.\.all\]\.ts\n/,
|
|
219
230
|
""
|
|
220
231
|
);
|
|
221
|
-
|
|
222
|
-
// Write the file
|
|
223
232
|
fs.writeFileSync(routeFile, template);
|
|
224
233
|
|
|
225
234
|
const prefix = useSrc ? "src/" : "";
|
|
@@ -245,7 +254,6 @@ async function config() {
|
|
|
245
254
|
const srcLabel = useSrc ? " (src/)" : "";
|
|
246
255
|
console.log(`📂 Detected: ${routerLabel}${srcLabel}\n`);
|
|
247
256
|
|
|
248
|
-
// Get Stripe API key (hidden input)
|
|
249
257
|
const existingStripeKey = process.env.STRIPE_SECRET_KEY || "";
|
|
250
258
|
const stripeSecretKey = await questionHidden(
|
|
251
259
|
null,
|
|
@@ -258,10 +266,8 @@ async function config() {
|
|
|
258
266
|
process.exit(1);
|
|
259
267
|
}
|
|
260
268
|
|
|
261
|
-
// Create readline for site URL question
|
|
262
269
|
const rl = createPrompt();
|
|
263
270
|
|
|
264
|
-
// Get site URL with default from env
|
|
265
271
|
const defaultSiteUrl = process.env.NEXT_PUBLIC_SITE_URL || "";
|
|
266
272
|
const siteUrl = await question(rl, "Enter your site URL", defaultSiteUrl);
|
|
267
273
|
|
|
@@ -271,7 +277,6 @@ async function config() {
|
|
|
271
277
|
process.exit(1);
|
|
272
278
|
}
|
|
273
279
|
|
|
274
|
-
// Validate URL
|
|
275
280
|
let webhookUrl;
|
|
276
281
|
try {
|
|
277
282
|
const url = new URL(siteUrl);
|
|
@@ -282,7 +287,6 @@ async function config() {
|
|
|
282
287
|
process.exit(1);
|
|
283
288
|
}
|
|
284
289
|
|
|
285
|
-
// Get DATABASE_URL (optional) - skip if already set in env
|
|
286
290
|
let databaseUrlInput = "";
|
|
287
291
|
if (process.env.DATABASE_URL) {
|
|
288
292
|
console.log("✓ DATABASE_URL already set in environment");
|
|
@@ -297,7 +301,6 @@ async function config() {
|
|
|
297
301
|
|
|
298
302
|
rl.close();
|
|
299
303
|
|
|
300
|
-
// Create the API route
|
|
301
304
|
console.log(`📁 Creating API route...`);
|
|
302
305
|
try {
|
|
303
306
|
const createdFile = createApiRoute(routerType, useSrc);
|
|
@@ -307,7 +310,6 @@ async function config() {
|
|
|
307
310
|
process.exit(1);
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
// Copy billing.config.ts to root
|
|
311
313
|
console.log(`📁 Creating billing.config.ts...`);
|
|
312
314
|
try {
|
|
313
315
|
const billingConfigPath = path.join(process.cwd(), "billing.config.ts");
|
|
@@ -329,7 +331,6 @@ async function config() {
|
|
|
329
331
|
const stripe = new Stripe(stripeSecretKey);
|
|
330
332
|
|
|
331
333
|
try {
|
|
332
|
-
// Check if a webhook with the same URL already exists
|
|
333
334
|
const existingWebhooks = await stripe.webhookEndpoints.list({ limit: 100 });
|
|
334
335
|
const existingWebhook = existingWebhooks.data.find(
|
|
335
336
|
(wh) => wh.url === webhookUrl
|
|
@@ -341,7 +342,6 @@ async function config() {
|
|
|
341
342
|
console.log(`✅ Deleted existing webhook (${existingWebhook.id})\n`);
|
|
342
343
|
}
|
|
343
344
|
|
|
344
|
-
// Create webhook endpoint
|
|
345
345
|
console.log(`🔄 Creating new webhook endpoint...`);
|
|
346
346
|
const webhook = await stripe.webhookEndpoints.create({
|
|
347
347
|
url: webhookUrl,
|
|
@@ -350,7 +350,6 @@ async function config() {
|
|
|
350
350
|
});
|
|
351
351
|
console.log("✅ Webhook created successfully!\n");
|
|
352
352
|
|
|
353
|
-
// Build list of env vars to update
|
|
354
353
|
const envVars = [
|
|
355
354
|
{ key: "STRIPE_SECRET_KEY", value: stripeSecretKey },
|
|
356
355
|
{ key: "STRIPE_WEBHOOK_SECRET", value: webhook.secret },
|
|
@@ -360,7 +359,6 @@ async function config() {
|
|
|
360
359
|
envVars.push({ key: "DATABASE_URL", value: databaseUrlInput });
|
|
361
360
|
}
|
|
362
361
|
|
|
363
|
-
// Save to env files
|
|
364
362
|
const updatedFiles = saveToEnvFiles(envVars);
|
|
365
363
|
|
|
366
364
|
const envVarNames = envVars.map((v) => v.key).join(", ");
|
|
@@ -401,7 +399,6 @@ async function config() {
|
|
|
401
399
|
}
|
|
402
400
|
|
|
403
401
|
function findMatchingBrace(content, startIndex) {
|
|
404
|
-
// Find the matching closing brace for an opening brace
|
|
405
402
|
let depth = 0;
|
|
406
403
|
for (let i = startIndex; i < content.length; i++) {
|
|
407
404
|
if (content[i] === "{" || content[i] === "[") depth++;
|
|
@@ -462,7 +459,6 @@ function parseBillingConfig(content, mode) {
|
|
|
462
459
|
return { config: null, plans: [] };
|
|
463
460
|
}
|
|
464
461
|
|
|
465
|
-
// Convert to JSON and parse
|
|
466
462
|
const jsonString = tsObjectToJson(extracted.raw);
|
|
467
463
|
let config;
|
|
468
464
|
try {
|
|
@@ -472,13 +468,11 @@ function parseBillingConfig(content, mode) {
|
|
|
472
468
|
return { config: null, plans: [] };
|
|
473
469
|
}
|
|
474
470
|
|
|
475
|
-
// Get plans for the specified mode
|
|
476
471
|
const modeConfig = config[mode];
|
|
477
472
|
if (!modeConfig || !modeConfig.plans || modeConfig.plans.length === 0) {
|
|
478
473
|
return { config, plans: [], extracted };
|
|
479
474
|
}
|
|
480
475
|
|
|
481
|
-
// Return parsed plans with their indices for updating
|
|
482
476
|
const plans = modeConfig.plans.map((plan, index) => ({
|
|
483
477
|
plan,
|
|
484
478
|
index,
|
|
@@ -488,7 +482,6 @@ function parseBillingConfig(content, mode) {
|
|
|
488
482
|
}
|
|
489
483
|
|
|
490
484
|
function reorderWithIdFirst(obj) {
|
|
491
|
-
// Reorder object so 'id' is the first property if it exists
|
|
492
485
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
493
486
|
return obj;
|
|
494
487
|
}
|
|
@@ -556,7 +549,6 @@ function formatConfigToTs(config) {
|
|
|
556
549
|
}
|
|
557
550
|
}
|
|
558
551
|
|
|
559
|
-
// Convert to TypeScript object literal format (unquoted keys)
|
|
560
552
|
return toTsObjectLiteral(reorderedConfig, 0);
|
|
561
553
|
}
|
|
562
554
|
|
|
@@ -578,7 +570,6 @@ async function sync() {
|
|
|
578
570
|
process.exit(1);
|
|
579
571
|
}
|
|
580
572
|
|
|
581
|
-
// Get Stripe API key from env or prompt
|
|
582
573
|
let stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
|
583
574
|
if (!stripeSecretKey) {
|
|
584
575
|
stripeSecretKey = await questionHidden(
|
|
@@ -592,7 +583,6 @@ async function sync() {
|
|
|
592
583
|
process.exit(1);
|
|
593
584
|
}
|
|
594
585
|
|
|
595
|
-
// Determine mode based on Stripe key
|
|
596
586
|
let mode;
|
|
597
587
|
try {
|
|
598
588
|
mode = getMode(stripeSecretKey);
|
|
@@ -613,7 +603,6 @@ async function sync() {
|
|
|
613
603
|
process.exit(1);
|
|
614
604
|
}
|
|
615
605
|
|
|
616
|
-
// Ensure the mode config exists with plans array
|
|
617
606
|
if (!config[mode]) {
|
|
618
607
|
config[mode] = { plans: [] };
|
|
619
608
|
}
|
|
@@ -629,20 +618,16 @@ async function sync() {
|
|
|
629
618
|
let skippedProducts = 0;
|
|
630
619
|
let skippedPrices = 0;
|
|
631
620
|
|
|
632
|
-
// === PULL: Fetch products and prices from Stripe and add missing ones ===
|
|
633
621
|
console.log("📥 Pulling products from Stripe...\n");
|
|
634
622
|
|
|
635
623
|
try {
|
|
636
|
-
// Fetch all active products from Stripe
|
|
637
624
|
const stripeProducts = await stripe.products.list({
|
|
638
625
|
active: true,
|
|
639
626
|
limit: 100,
|
|
640
627
|
});
|
|
641
628
|
|
|
642
|
-
// Fetch all active prices from Stripe
|
|
643
629
|
const stripePrices = await stripe.prices.list({ active: true, limit: 100 });
|
|
644
630
|
|
|
645
|
-
// Build a map of price by product
|
|
646
631
|
const pricesByProduct = {};
|
|
647
632
|
for (const price of stripePrices.data) {
|
|
648
633
|
const productId =
|
|
@@ -653,12 +638,10 @@ async function sync() {
|
|
|
653
638
|
pricesByProduct[productId].push(price);
|
|
654
639
|
}
|
|
655
640
|
|
|
656
|
-
// Get existing product IDs in config
|
|
657
641
|
const existingProductIds = new Set(
|
|
658
642
|
config[mode].plans.filter((p) => p.id).map((p) => p.id)
|
|
659
643
|
);
|
|
660
644
|
|
|
661
|
-
// Get existing price IDs in config
|
|
662
645
|
const existingPriceIds = new Set();
|
|
663
646
|
for (const plan of config[mode].plans) {
|
|
664
647
|
if (plan.price) {
|
|
@@ -749,10 +732,8 @@ async function sync() {
|
|
|
749
732
|
console.error("❌ Failed to fetch products from Stripe:", error.message);
|
|
750
733
|
}
|
|
751
734
|
|
|
752
|
-
// === PUSH: Create products and prices in Stripe from config ===
|
|
753
735
|
console.log("📤 Pushing new plans to Stripe...\n");
|
|
754
736
|
|
|
755
|
-
// Re-get plans after potential modifications
|
|
756
737
|
const currentPlans = config[mode].plans || [];
|
|
757
738
|
|
|
758
739
|
if (currentPlans.length === 0) {
|
|
@@ -843,7 +824,6 @@ async function sync() {
|
|
|
843
824
|
console.log(" No new products or prices to push to Stripe.\n");
|
|
844
825
|
}
|
|
845
826
|
|
|
846
|
-
// Write updated config back to file
|
|
847
827
|
if (configModified) {
|
|
848
828
|
const newConfigJson = formatConfigToTs(config);
|
|
849
829
|
const newContent =
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,11 @@ import Stripe from 'stripe';
|
|
|
2
2
|
import { B as BillingConfig, P as PriceInterval } from './BillingConfig-n6VbfqGY.mjs';
|
|
3
3
|
export { b as Plan, a as Price } from './BillingConfig-n6VbfqGY.mjs';
|
|
4
4
|
|
|
5
|
+
interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
}
|
|
5
10
|
interface StripeWebhookCallbacks {
|
|
6
11
|
/**
|
|
7
12
|
* Called when a new subscription is created
|
|
@@ -52,6 +57,16 @@ interface StripeHandlerConfig {
|
|
|
52
57
|
* Callbacks for subscription events
|
|
53
58
|
*/
|
|
54
59
|
callbacks?: StripeWebhookCallbacks;
|
|
60
|
+
/**
|
|
61
|
+
* Function to map a user ID to a Stripe customer ID.
|
|
62
|
+
* Used as fallback when user is not found in user_stripe_customer_map table.
|
|
63
|
+
*/
|
|
64
|
+
mapUserIdToStripeCustomerId?: (userId: string) => string | Promise<string> | null | Promise<string | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Function to extract user from the request.
|
|
67
|
+
* Useful for extracting user from authentication middleware/session.
|
|
68
|
+
*/
|
|
69
|
+
getUser?: (request: Request) => User | Promise<User> | null | Promise<User | null>;
|
|
55
70
|
}
|
|
56
71
|
interface CheckoutRequestBody {
|
|
57
72
|
/**
|
|
@@ -90,11 +105,30 @@ interface CheckoutRequestBody {
|
|
|
90
105
|
* Existing Stripe customer ID
|
|
91
106
|
*/
|
|
92
107
|
customerId?: string;
|
|
108
|
+
/**
|
|
109
|
+
* User object to associate with this checkout.
|
|
110
|
+
* Will be used to look up or create a Stripe customer.
|
|
111
|
+
*/
|
|
112
|
+
user?: User;
|
|
93
113
|
/**
|
|
94
114
|
* Additional metadata to attach to the session
|
|
95
115
|
*/
|
|
96
116
|
metadata?: Record<string, string>;
|
|
97
117
|
}
|
|
118
|
+
interface CustomerPortalRequestBody {
|
|
119
|
+
/**
|
|
120
|
+
* Stripe customer ID (cus_...)
|
|
121
|
+
*/
|
|
122
|
+
stripe_customer_id?: string;
|
|
123
|
+
/**
|
|
124
|
+
* User object to look up Stripe customer ID
|
|
125
|
+
*/
|
|
126
|
+
user?: User;
|
|
127
|
+
/**
|
|
128
|
+
* URL to redirect to after the customer portal session ends
|
|
129
|
+
*/
|
|
130
|
+
returnUrl?: string;
|
|
131
|
+
}
|
|
98
132
|
declare function createStripeHandler(config?: StripeHandlerConfig): (request: Request) => Promise<Response>;
|
|
99
133
|
|
|
100
|
-
export { BillingConfig, type CheckoutRequestBody, PriceInterval, type StripeHandlerConfig, type StripeWebhookCallbacks, createStripeHandler };
|
|
134
|
+
export { BillingConfig, type CheckoutRequestBody, type CustomerPortalRequestBody, PriceInterval, type StripeHandlerConfig, type StripeWebhookCallbacks, type User, createStripeHandler };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import Stripe from 'stripe';
|
|
|
2
2
|
import { B as BillingConfig, P as PriceInterval } from './BillingConfig-n6VbfqGY.js';
|
|
3
3
|
export { b as Plan, a as Price } from './BillingConfig-n6VbfqGY.js';
|
|
4
4
|
|
|
5
|
+
interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
}
|
|
5
10
|
interface StripeWebhookCallbacks {
|
|
6
11
|
/**
|
|
7
12
|
* Called when a new subscription is created
|
|
@@ -52,6 +57,16 @@ interface StripeHandlerConfig {
|
|
|
52
57
|
* Callbacks for subscription events
|
|
53
58
|
*/
|
|
54
59
|
callbacks?: StripeWebhookCallbacks;
|
|
60
|
+
/**
|
|
61
|
+
* Function to map a user ID to a Stripe customer ID.
|
|
62
|
+
* Used as fallback when user is not found in user_stripe_customer_map table.
|
|
63
|
+
*/
|
|
64
|
+
mapUserIdToStripeCustomerId?: (userId: string) => string | Promise<string> | null | Promise<string | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Function to extract user from the request.
|
|
67
|
+
* Useful for extracting user from authentication middleware/session.
|
|
68
|
+
*/
|
|
69
|
+
getUser?: (request: Request) => User | Promise<User> | null | Promise<User | null>;
|
|
55
70
|
}
|
|
56
71
|
interface CheckoutRequestBody {
|
|
57
72
|
/**
|
|
@@ -90,11 +105,30 @@ interface CheckoutRequestBody {
|
|
|
90
105
|
* Existing Stripe customer ID
|
|
91
106
|
*/
|
|
92
107
|
customerId?: string;
|
|
108
|
+
/**
|
|
109
|
+
* User object to associate with this checkout.
|
|
110
|
+
* Will be used to look up or create a Stripe customer.
|
|
111
|
+
*/
|
|
112
|
+
user?: User;
|
|
93
113
|
/**
|
|
94
114
|
* Additional metadata to attach to the session
|
|
95
115
|
*/
|
|
96
116
|
metadata?: Record<string, string>;
|
|
97
117
|
}
|
|
118
|
+
interface CustomerPortalRequestBody {
|
|
119
|
+
/**
|
|
120
|
+
* Stripe customer ID (cus_...)
|
|
121
|
+
*/
|
|
122
|
+
stripe_customer_id?: string;
|
|
123
|
+
/**
|
|
124
|
+
* User object to look up Stripe customer ID
|
|
125
|
+
*/
|
|
126
|
+
user?: User;
|
|
127
|
+
/**
|
|
128
|
+
* URL to redirect to after the customer portal session ends
|
|
129
|
+
*/
|
|
130
|
+
returnUrl?: string;
|
|
131
|
+
}
|
|
98
132
|
declare function createStripeHandler(config?: StripeHandlerConfig): (request: Request) => Promise<Response>;
|
|
99
133
|
|
|
100
|
-
export { BillingConfig, type CheckoutRequestBody, PriceInterval, type StripeHandlerConfig, type StripeWebhookCallbacks, createStripeHandler };
|
|
134
|
+
export { BillingConfig, type CheckoutRequestBody, type CustomerPortalRequestBody, PriceInterval, type StripeHandlerConfig, type StripeWebhookCallbacks, type User, createStripeHandler };
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
37
37
|
// src/handler.ts
|
|
38
38
|
var import_stripe_sync_engine = require("@supabase/stripe-sync-engine");
|
|
39
39
|
var import_stripe = __toESM(require("stripe"));
|
|
40
|
+
var import_pg = require("pg");
|
|
40
41
|
|
|
41
42
|
// src/utils.ts
|
|
42
43
|
var getMode = (stripeKey) => {
|
|
@@ -60,9 +61,12 @@ function createStripeHandler(config = {}) {
|
|
|
60
61
|
successUrl: defaultSuccessUrl,
|
|
61
62
|
cancelUrl: defaultCancelUrl,
|
|
62
63
|
automaticTax = true,
|
|
63
|
-
callbacks
|
|
64
|
+
callbacks,
|
|
65
|
+
mapUserIdToStripeCustomerId,
|
|
66
|
+
getUser
|
|
64
67
|
} = config;
|
|
65
68
|
const stripe = new import_stripe.default(stripeSecretKey);
|
|
69
|
+
const pool = databaseUrl ? new import_pg.Pool({ connectionString: databaseUrl }) : null;
|
|
66
70
|
const sync = databaseUrl ? new import_stripe_sync_engine.StripeSync({
|
|
67
71
|
poolConfig: {
|
|
68
72
|
connectionString: databaseUrl
|
|
@@ -71,6 +75,47 @@ function createStripeHandler(config = {}) {
|
|
|
71
75
|
stripeSecretKey,
|
|
72
76
|
stripeWebhookSecret
|
|
73
77
|
}) : null;
|
|
78
|
+
async function resolveStripeCustomerId(options) {
|
|
79
|
+
const { user, createIfNotFound } = options;
|
|
80
|
+
const { id: userId, name, email } = user;
|
|
81
|
+
if (pool) {
|
|
82
|
+
const result = await pool.query(
|
|
83
|
+
`SELECT stripe_customer_id FROM ${schema}.user_stripe_customer_map WHERE user_id = $1`,
|
|
84
|
+
[userId]
|
|
85
|
+
);
|
|
86
|
+
if (result.rows.length > 0) {
|
|
87
|
+
return result.rows[0].stripe_customer_id;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (mapUserIdToStripeCustomerId) {
|
|
91
|
+
const customerId = await mapUserIdToStripeCustomerId(userId);
|
|
92
|
+
if (customerId) {
|
|
93
|
+
return customerId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (createIfNotFound) {
|
|
97
|
+
const customerParams = {
|
|
98
|
+
metadata: { user_id: userId }
|
|
99
|
+
};
|
|
100
|
+
if (name) {
|
|
101
|
+
customerParams.name = name;
|
|
102
|
+
}
|
|
103
|
+
if (email) {
|
|
104
|
+
customerParams.email = email;
|
|
105
|
+
}
|
|
106
|
+
const customer = await stripe.customers.create(customerParams);
|
|
107
|
+
if (pool) {
|
|
108
|
+
await pool.query(
|
|
109
|
+
`INSERT INTO ${schema}.user_stripe_customer_map (user_id, stripe_customer_id)
|
|
110
|
+
VALUES ($1, $2)
|
|
111
|
+
ON CONFLICT (user_id) DO UPDATE SET stripe_customer_id = $2, updated_at = now()`,
|
|
112
|
+
[userId, customer.id]
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return customer.id;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
74
119
|
function resolvePriceId(body, mode) {
|
|
75
120
|
if (body.priceId) {
|
|
76
121
|
return body.priceId;
|
|
@@ -133,11 +178,25 @@ function createStripeHandler(config = {}) {
|
|
|
133
178
|
cancel_url: cancelUrl,
|
|
134
179
|
automatic_tax: { enabled: automaticTax }
|
|
135
180
|
};
|
|
136
|
-
|
|
137
|
-
sessionParams.customer_email = body.customerEmail;
|
|
138
|
-
}
|
|
181
|
+
let customerId = null;
|
|
139
182
|
if (body.customerId) {
|
|
140
|
-
|
|
183
|
+
customerId = body.customerId;
|
|
184
|
+
} else {
|
|
185
|
+
let user = body.user;
|
|
186
|
+
if (!user && getUser) {
|
|
187
|
+
user = await getUser(request) ?? void 0;
|
|
188
|
+
}
|
|
189
|
+
if (user) {
|
|
190
|
+
customerId = await resolveStripeCustomerId({
|
|
191
|
+
user,
|
|
192
|
+
createIfNotFound: true
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (customerId) {
|
|
197
|
+
sessionParams.customer = customerId;
|
|
198
|
+
} else if (body.customerEmail) {
|
|
199
|
+
sessionParams.customer_email = body.customerEmail;
|
|
141
200
|
}
|
|
142
201
|
if (body.metadata) {
|
|
143
202
|
sessionParams.metadata = body.metadata;
|
|
@@ -212,6 +271,56 @@ function createStripeHandler(config = {}) {
|
|
|
212
271
|
return new Response(message, { status: 500 });
|
|
213
272
|
}
|
|
214
273
|
}
|
|
274
|
+
async function handleCustomerPortal(request) {
|
|
275
|
+
try {
|
|
276
|
+
const body = await request.json();
|
|
277
|
+
let customerId = null;
|
|
278
|
+
if (body.stripe_customer_id) {
|
|
279
|
+
customerId = body.stripe_customer_id;
|
|
280
|
+
} else {
|
|
281
|
+
let user = body.user;
|
|
282
|
+
if (!user && getUser) {
|
|
283
|
+
user = await getUser(request) ?? void 0;
|
|
284
|
+
}
|
|
285
|
+
if (user) {
|
|
286
|
+
customerId = await resolveStripeCustomerId({
|
|
287
|
+
user,
|
|
288
|
+
createIfNotFound: false
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (!customerId) {
|
|
293
|
+
return new Response(
|
|
294
|
+
JSON.stringify({
|
|
295
|
+
error: "Provide either stripe_customer_id or user. Alternatively, configure getUser to extract user from the request."
|
|
296
|
+
}),
|
|
297
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const origin = request.headers.get("origin") || "";
|
|
301
|
+
const returnUrl = body.returnUrl || `${origin}/`;
|
|
302
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
303
|
+
customer: customerId,
|
|
304
|
+
return_url: returnUrl
|
|
305
|
+
});
|
|
306
|
+
const acceptHeader = request.headers.get("accept") || "";
|
|
307
|
+
if (acceptHeader.includes("application/json")) {
|
|
308
|
+
return new Response(JSON.stringify({ url: session.url }), {
|
|
309
|
+
status: 200,
|
|
310
|
+
headers: { "Content-Type": "application/json" }
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return Response.redirect(session.url, 303);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error("Customer portal error:", err);
|
|
316
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
317
|
+
const status = err && typeof err === "object" && "statusCode" in err ? err.statusCode : 500;
|
|
318
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
319
|
+
status,
|
|
320
|
+
headers: { "Content-Type": "application/json" }
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
215
324
|
return async function handler(request) {
|
|
216
325
|
const url = new URL(request.url);
|
|
217
326
|
const pathSegments = url.pathname.split("/").filter(Boolean);
|
|
@@ -227,10 +336,12 @@ function createStripeHandler(config = {}) {
|
|
|
227
336
|
return handleCheckout(request);
|
|
228
337
|
case "webhook":
|
|
229
338
|
return handleWebhook(request);
|
|
339
|
+
case "customer_portal":
|
|
340
|
+
return handleCustomerPortal(request);
|
|
230
341
|
default:
|
|
231
342
|
return new Response(
|
|
232
343
|
JSON.stringify({
|
|
233
|
-
error: `Unknown action: ${action}. Supported: checkout, webhook`
|
|
344
|
+
error: `Unknown action: ${action}. Supported: checkout, webhook, customer_portal`
|
|
234
345
|
}),
|
|
235
346
|
{ status: 404, headers: { "Content-Type": "application/json" } }
|
|
236
347
|
);
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/handler.ts
|
|
2
2
|
import { StripeSync } from "@supabase/stripe-sync-engine";
|
|
3
3
|
import Stripe from "stripe";
|
|
4
|
+
import { Pool } from "pg";
|
|
4
5
|
|
|
5
6
|
// src/utils.ts
|
|
6
7
|
var getMode = (stripeKey) => {
|
|
@@ -24,9 +25,12 @@ function createStripeHandler(config = {}) {
|
|
|
24
25
|
successUrl: defaultSuccessUrl,
|
|
25
26
|
cancelUrl: defaultCancelUrl,
|
|
26
27
|
automaticTax = true,
|
|
27
|
-
callbacks
|
|
28
|
+
callbacks,
|
|
29
|
+
mapUserIdToStripeCustomerId,
|
|
30
|
+
getUser
|
|
28
31
|
} = config;
|
|
29
32
|
const stripe = new Stripe(stripeSecretKey);
|
|
33
|
+
const pool = databaseUrl ? new Pool({ connectionString: databaseUrl }) : null;
|
|
30
34
|
const sync = databaseUrl ? new StripeSync({
|
|
31
35
|
poolConfig: {
|
|
32
36
|
connectionString: databaseUrl
|
|
@@ -35,6 +39,47 @@ function createStripeHandler(config = {}) {
|
|
|
35
39
|
stripeSecretKey,
|
|
36
40
|
stripeWebhookSecret
|
|
37
41
|
}) : null;
|
|
42
|
+
async function resolveStripeCustomerId(options) {
|
|
43
|
+
const { user, createIfNotFound } = options;
|
|
44
|
+
const { id: userId, name, email } = user;
|
|
45
|
+
if (pool) {
|
|
46
|
+
const result = await pool.query(
|
|
47
|
+
`SELECT stripe_customer_id FROM ${schema}.user_stripe_customer_map WHERE user_id = $1`,
|
|
48
|
+
[userId]
|
|
49
|
+
);
|
|
50
|
+
if (result.rows.length > 0) {
|
|
51
|
+
return result.rows[0].stripe_customer_id;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (mapUserIdToStripeCustomerId) {
|
|
55
|
+
const customerId = await mapUserIdToStripeCustomerId(userId);
|
|
56
|
+
if (customerId) {
|
|
57
|
+
return customerId;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (createIfNotFound) {
|
|
61
|
+
const customerParams = {
|
|
62
|
+
metadata: { user_id: userId }
|
|
63
|
+
};
|
|
64
|
+
if (name) {
|
|
65
|
+
customerParams.name = name;
|
|
66
|
+
}
|
|
67
|
+
if (email) {
|
|
68
|
+
customerParams.email = email;
|
|
69
|
+
}
|
|
70
|
+
const customer = await stripe.customers.create(customerParams);
|
|
71
|
+
if (pool) {
|
|
72
|
+
await pool.query(
|
|
73
|
+
`INSERT INTO ${schema}.user_stripe_customer_map (user_id, stripe_customer_id)
|
|
74
|
+
VALUES ($1, $2)
|
|
75
|
+
ON CONFLICT (user_id) DO UPDATE SET stripe_customer_id = $2, updated_at = now()`,
|
|
76
|
+
[userId, customer.id]
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return customer.id;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
38
83
|
function resolvePriceId(body, mode) {
|
|
39
84
|
if (body.priceId) {
|
|
40
85
|
return body.priceId;
|
|
@@ -97,11 +142,25 @@ function createStripeHandler(config = {}) {
|
|
|
97
142
|
cancel_url: cancelUrl,
|
|
98
143
|
automatic_tax: { enabled: automaticTax }
|
|
99
144
|
};
|
|
100
|
-
|
|
101
|
-
sessionParams.customer_email = body.customerEmail;
|
|
102
|
-
}
|
|
145
|
+
let customerId = null;
|
|
103
146
|
if (body.customerId) {
|
|
104
|
-
|
|
147
|
+
customerId = body.customerId;
|
|
148
|
+
} else {
|
|
149
|
+
let user = body.user;
|
|
150
|
+
if (!user && getUser) {
|
|
151
|
+
user = await getUser(request) ?? void 0;
|
|
152
|
+
}
|
|
153
|
+
if (user) {
|
|
154
|
+
customerId = await resolveStripeCustomerId({
|
|
155
|
+
user,
|
|
156
|
+
createIfNotFound: true
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (customerId) {
|
|
161
|
+
sessionParams.customer = customerId;
|
|
162
|
+
} else if (body.customerEmail) {
|
|
163
|
+
sessionParams.customer_email = body.customerEmail;
|
|
105
164
|
}
|
|
106
165
|
if (body.metadata) {
|
|
107
166
|
sessionParams.metadata = body.metadata;
|
|
@@ -176,6 +235,56 @@ function createStripeHandler(config = {}) {
|
|
|
176
235
|
return new Response(message, { status: 500 });
|
|
177
236
|
}
|
|
178
237
|
}
|
|
238
|
+
async function handleCustomerPortal(request) {
|
|
239
|
+
try {
|
|
240
|
+
const body = await request.json();
|
|
241
|
+
let customerId = null;
|
|
242
|
+
if (body.stripe_customer_id) {
|
|
243
|
+
customerId = body.stripe_customer_id;
|
|
244
|
+
} else {
|
|
245
|
+
let user = body.user;
|
|
246
|
+
if (!user && getUser) {
|
|
247
|
+
user = await getUser(request) ?? void 0;
|
|
248
|
+
}
|
|
249
|
+
if (user) {
|
|
250
|
+
customerId = await resolveStripeCustomerId({
|
|
251
|
+
user,
|
|
252
|
+
createIfNotFound: false
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!customerId) {
|
|
257
|
+
return new Response(
|
|
258
|
+
JSON.stringify({
|
|
259
|
+
error: "Provide either stripe_customer_id or user. Alternatively, configure getUser to extract user from the request."
|
|
260
|
+
}),
|
|
261
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const origin = request.headers.get("origin") || "";
|
|
265
|
+
const returnUrl = body.returnUrl || `${origin}/`;
|
|
266
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
267
|
+
customer: customerId,
|
|
268
|
+
return_url: returnUrl
|
|
269
|
+
});
|
|
270
|
+
const acceptHeader = request.headers.get("accept") || "";
|
|
271
|
+
if (acceptHeader.includes("application/json")) {
|
|
272
|
+
return new Response(JSON.stringify({ url: session.url }), {
|
|
273
|
+
status: 200,
|
|
274
|
+
headers: { "Content-Type": "application/json" }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return Response.redirect(session.url, 303);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error("Customer portal error:", err);
|
|
280
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
281
|
+
const status = err && typeof err === "object" && "statusCode" in err ? err.statusCode : 500;
|
|
282
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
283
|
+
status,
|
|
284
|
+
headers: { "Content-Type": "application/json" }
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
179
288
|
return async function handler(request) {
|
|
180
289
|
const url = new URL(request.url);
|
|
181
290
|
const pathSegments = url.pathname.split("/").filter(Boolean);
|
|
@@ -191,10 +300,12 @@ function createStripeHandler(config = {}) {
|
|
|
191
300
|
return handleCheckout(request);
|
|
192
301
|
case "webhook":
|
|
193
302
|
return handleWebhook(request);
|
|
303
|
+
case "customer_portal":
|
|
304
|
+
return handleCustomerPortal(request);
|
|
194
305
|
default:
|
|
195
306
|
return new Response(
|
|
196
307
|
JSON.stringify({
|
|
197
|
-
error: `Unknown action: ${action}. Supported: checkout, webhook`
|
|
308
|
+
error: `Unknown action: ${action}. Supported: checkout, webhook, customer_portal`
|
|
198
309
|
}),
|
|
199
310
|
{ status: 404, headers: { "Content-Type": "application/json" } }
|
|
200
311
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stripe-no-webhooks",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"author": "Ramon Garate",
|
|
5
5
|
"description": "Stripe integration without dealing with webhooks",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,13 +34,15 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@supabase/stripe-sync-engine": "^0.47.0",
|
|
37
|
-
"dotenv": "^17.2.3"
|
|
37
|
+
"dotenv": "^17.2.3",
|
|
38
|
+
"pg": "^8.13.1"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
41
|
"stripe": ">=14.0.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@types/node": "^22.10.2",
|
|
45
|
+
"@types/pg": "^8.16.0",
|
|
44
46
|
"stripe": "^17.4.0",
|
|
45
47
|
"tsup": "^8.3.5",
|
|
46
48
|
"typescript": "^5.7.2"
|