stripe-no-webhooks 0.0.2 → 0.0.4
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/dist/index.mjs
CHANGED
|
@@ -1,24 +1,124 @@
|
|
|
1
1
|
// src/handler.ts
|
|
2
2
|
import { StripeSync } from "@supabase/stripe-sync-engine";
|
|
3
3
|
import Stripe from "stripe";
|
|
4
|
-
function
|
|
4
|
+
function createStripeHandler(config = {}) {
|
|
5
5
|
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
stripeSecretKey = process.env.STRIPE_SECRET_KEY,
|
|
7
|
+
stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET,
|
|
8
|
+
databaseUrl = process.env.DATABASE_URL,
|
|
9
9
|
schema = "stripe",
|
|
10
|
+
billingConfig,
|
|
11
|
+
successUrl: defaultSuccessUrl,
|
|
12
|
+
cancelUrl: defaultCancelUrl,
|
|
13
|
+
automaticTax = true,
|
|
10
14
|
callbacks
|
|
11
15
|
} = config;
|
|
12
|
-
const
|
|
16
|
+
const stripe = new Stripe(stripeSecretKey);
|
|
17
|
+
const sync = databaseUrl ? new StripeSync({
|
|
13
18
|
poolConfig: {
|
|
14
19
|
connectionString: databaseUrl
|
|
15
20
|
},
|
|
16
21
|
schema,
|
|
17
22
|
stripeSecretKey,
|
|
18
23
|
stripeWebhookSecret
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
}) : null;
|
|
25
|
+
function resolvePriceId(body) {
|
|
26
|
+
if (body.priceId) {
|
|
27
|
+
return body.priceId;
|
|
28
|
+
}
|
|
29
|
+
if (!body.interval) {
|
|
30
|
+
throw new Error("interval is required when using planName or planId");
|
|
31
|
+
}
|
|
32
|
+
if (!billingConfig?.plans) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"billingConfig with plans is required when using planName or planId"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const plan = body.planName ? billingConfig.plans.find((p) => p.name === body.planName) : body.planId ? billingConfig.plans.find((p) => p.id === body.planId) : null;
|
|
38
|
+
if (!plan) {
|
|
39
|
+
const identifier = body.planName || body.planId;
|
|
40
|
+
throw new Error(`Plan not found: ${identifier}`);
|
|
41
|
+
}
|
|
42
|
+
const price = plan.price.find((p) => p.interval === body.interval);
|
|
43
|
+
if (!price) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Price with interval "${body.interval}" not found for plan "${plan.name}"`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (!price.id) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Price ID not set for plan "${plan.name}" with interval "${body.interval}". Run stripe-sync to sync price IDs.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return price.id;
|
|
54
|
+
}
|
|
55
|
+
async function getPriceMode(priceId) {
|
|
56
|
+
const price = await stripe.prices.retrieve(priceId);
|
|
57
|
+
return price.type === "recurring" ? "subscription" : "payment";
|
|
58
|
+
}
|
|
59
|
+
async function handleCheckout(request) {
|
|
60
|
+
try {
|
|
61
|
+
const body = await request.json();
|
|
62
|
+
if (!body.priceId && !body.planName && !body.planId) {
|
|
63
|
+
return new Response(
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
error: "Provide either priceId, planName+interval, or planId+interval"
|
|
66
|
+
}),
|
|
67
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const origin = request.headers.get("origin") || "";
|
|
71
|
+
const successUrl = body.successUrl || defaultSuccessUrl || `${origin}/success?session_id={CHECKOUT_SESSION_ID}`;
|
|
72
|
+
const cancelUrl = body.cancelUrl || defaultCancelUrl || `${origin}/`;
|
|
73
|
+
const priceId = resolvePriceId(body);
|
|
74
|
+
const mode = await getPriceMode(priceId);
|
|
75
|
+
const sessionParams = {
|
|
76
|
+
line_items: [
|
|
77
|
+
{
|
|
78
|
+
price: priceId,
|
|
79
|
+
quantity: body.quantity ?? 1
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
mode,
|
|
83
|
+
success_url: successUrl,
|
|
84
|
+
cancel_url: cancelUrl,
|
|
85
|
+
automatic_tax: { enabled: automaticTax }
|
|
86
|
+
};
|
|
87
|
+
if (body.customerEmail) {
|
|
88
|
+
sessionParams.customer_email = body.customerEmail;
|
|
89
|
+
}
|
|
90
|
+
if (body.customerId) {
|
|
91
|
+
sessionParams.customer = body.customerId;
|
|
92
|
+
}
|
|
93
|
+
if (body.metadata) {
|
|
94
|
+
sessionParams.metadata = body.metadata;
|
|
95
|
+
}
|
|
96
|
+
const session = await stripe.checkout.sessions.create(sessionParams);
|
|
97
|
+
if (!session.url) {
|
|
98
|
+
return new Response(
|
|
99
|
+
JSON.stringify({ error: "Failed to create checkout session" }),
|
|
100
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const acceptHeader = request.headers.get("accept") || "";
|
|
104
|
+
if (acceptHeader.includes("application/json")) {
|
|
105
|
+
return new Response(JSON.stringify({ url: session.url }), {
|
|
106
|
+
status: 200,
|
|
107
|
+
headers: { "Content-Type": "application/json" }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return Response.redirect(session.url, 303);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error("Checkout error:", err);
|
|
113
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
114
|
+
const status = err && typeof err === "object" && "statusCode" in err ? err.statusCode : 500;
|
|
115
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
116
|
+
status,
|
|
117
|
+
headers: { "Content-Type": "application/json" }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function handleWebhook(request) {
|
|
22
122
|
try {
|
|
23
123
|
const body = await request.text();
|
|
24
124
|
const signature = request.headers.get("stripe-signature");
|
|
@@ -39,7 +139,9 @@ function createStripeWebhookHandler(config) {
|
|
|
39
139
|
{ status: 400 }
|
|
40
140
|
);
|
|
41
141
|
}
|
|
42
|
-
|
|
142
|
+
if (sync) {
|
|
143
|
+
await sync.processWebhook(body, signature);
|
|
144
|
+
}
|
|
43
145
|
if (callbacks) {
|
|
44
146
|
await handleCallbacks(event, callbacks);
|
|
45
147
|
}
|
|
@@ -52,6 +154,30 @@ function createStripeWebhookHandler(config) {
|
|
|
52
154
|
const message = error instanceof Error ? error.message : "Internal server error";
|
|
53
155
|
return new Response(message, { status: 500 });
|
|
54
156
|
}
|
|
157
|
+
}
|
|
158
|
+
return async function handler(request) {
|
|
159
|
+
const url = new URL(request.url);
|
|
160
|
+
const pathSegments = url.pathname.split("/").filter(Boolean);
|
|
161
|
+
const action = pathSegments[pathSegments.length - 1];
|
|
162
|
+
if (request.method !== "POST") {
|
|
163
|
+
return new Response(JSON.stringify({ error: "Method not allowed" }), {
|
|
164
|
+
status: 405,
|
|
165
|
+
headers: { "Content-Type": "application/json" }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
switch (action) {
|
|
169
|
+
case "checkout":
|
|
170
|
+
return handleCheckout(request);
|
|
171
|
+
case "webhook":
|
|
172
|
+
return handleWebhook(request);
|
|
173
|
+
default:
|
|
174
|
+
return new Response(
|
|
175
|
+
JSON.stringify({
|
|
176
|
+
error: `Unknown action: ${action}. Supported: checkout, webhook`
|
|
177
|
+
}),
|
|
178
|
+
{ status: 404, headers: { "Content-Type": "application/json" } }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
55
181
|
};
|
|
56
182
|
}
|
|
57
183
|
async function handleCallbacks(event, callbacks) {
|
|
@@ -77,5 +203,5 @@ async function handleCallbacks(event, callbacks) {
|
|
|
77
203
|
}
|
|
78
204
|
}
|
|
79
205
|
export {
|
|
80
|
-
|
|
206
|
+
createStripeHandler
|
|
81
207
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stripe-no-webhooks",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"author": "Ramon Garate",
|
|
5
5
|
"description": "Stripe integration without dealing with webhooks",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.mjs",
|
|
13
13
|
"require": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./client": {
|
|
16
|
+
"types": "./dist/client.d.ts",
|
|
17
|
+
"import": "./dist/client.mjs",
|
|
18
|
+
"require": "./dist/client.js"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"bin": {
|
|
@@ -18,16 +23,18 @@
|
|
|
18
23
|
},
|
|
19
24
|
"files": [
|
|
20
25
|
"dist",
|
|
21
|
-
"bin"
|
|
26
|
+
"bin",
|
|
27
|
+
"src/templates"
|
|
22
28
|
],
|
|
23
29
|
"scripts": {
|
|
24
|
-
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
25
|
-
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
30
|
+
"build": "tsup src/index.ts src/client.ts --format cjs,esm --dts",
|
|
31
|
+
"dev": "tsup src/index.ts src/client.ts --format cjs,esm --dts --watch",
|
|
26
32
|
"prepublishOnly": "npm run build",
|
|
27
|
-
"
|
|
33
|
+
"release": "npm version patch && npm publish"
|
|
28
34
|
},
|
|
29
35
|
"dependencies": {
|
|
30
|
-
"@supabase/stripe-sync-engine": "^0.47.0"
|
|
36
|
+
"@supabase/stripe-sync-engine": "^0.47.0",
|
|
37
|
+
"dotenv": "^17.2.3"
|
|
31
38
|
},
|
|
32
39
|
"peerDependencies": {
|
|
33
40
|
"stripe": ">=14.0.0"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// app/api/stripe/[...all]/route.ts
|
|
2
|
+
import { createStripeHandler } from "stripe-no-webhooks";
|
|
3
|
+
import type { Stripe } from "stripe";
|
|
4
|
+
import billingConfig from "../../../../billing.config";
|
|
5
|
+
|
|
6
|
+
export const POST = createStripeHandler({
|
|
7
|
+
billingConfig,
|
|
8
|
+
callbacks: {
|
|
9
|
+
onSubscriptionCreated: async (subscription: Stripe.Subscription) => {
|
|
10
|
+
// Called when a new subscription is created
|
|
11
|
+
console.log("New subscription:", subscription.id);
|
|
12
|
+
// e.g., send welcome email, provision resources, etc.
|
|
13
|
+
},
|
|
14
|
+
onSubscriptionCancelled: async (subscription: Stripe.Subscription) => {
|
|
15
|
+
// Called when a subscription is cancelled
|
|
16
|
+
console.log("Subscription cancelled:", subscription.id);
|
|
17
|
+
// e.g., send cancellation email, revoke access, etc.
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BillingConfig } from "stripe-no-webhooks";
|
|
2
|
+
|
|
3
|
+
const billingConfig: BillingConfig = {
|
|
4
|
+
/*
|
|
5
|
+
plans: [
|
|
6
|
+
{
|
|
7
|
+
name: "Premium",
|
|
8
|
+
description: "Access to all features",
|
|
9
|
+
price: [
|
|
10
|
+
{
|
|
11
|
+
amount: 1000, // in cents, 1000 = $10.00
|
|
12
|
+
currency: "usd",
|
|
13
|
+
interval: "month",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
amount: 10000, // in cents, 10000 = $100.00
|
|
17
|
+
currency: "usd",
|
|
18
|
+
interval: "year",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
*/
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default billingConfig;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// pages/api/stripe/[...all].ts
|
|
2
|
+
import { createStripeHandler } from "stripe-no-webhooks";
|
|
3
|
+
import type { NextApiRequest, NextApiResponse } from "next";
|
|
4
|
+
import type { Stripe } from "stripe";
|
|
5
|
+
import billingConfig from "../../../billing.config";
|
|
6
|
+
|
|
7
|
+
const handler = createStripeHandler({
|
|
8
|
+
billingConfig,
|
|
9
|
+
callbacks: {
|
|
10
|
+
onSubscriptionCreated: async (subscription: Stripe.Subscription) => {
|
|
11
|
+
// Called when a new subscription is created
|
|
12
|
+
console.log("New subscription:", subscription.id);
|
|
13
|
+
// e.g., send welcome email, provision resources, etc.
|
|
14
|
+
},
|
|
15
|
+
onSubscriptionCancelled: async (subscription: Stripe.Subscription) => {
|
|
16
|
+
// Called when a subscription is cancelled
|
|
17
|
+
console.log("Subscription cancelled:", subscription.id);
|
|
18
|
+
// e.g., send cancellation email, revoke access, etc.
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Disable body parsing, we need the raw body for webhook verification
|
|
24
|
+
export const config = {
|
|
25
|
+
api: {
|
|
26
|
+
bodyParser: false,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default async function stripeHandler(
|
|
31
|
+
req: NextApiRequest,
|
|
32
|
+
res: NextApiResponse
|
|
33
|
+
) {
|
|
34
|
+
// Convert NextApiRequest to Request for the handler
|
|
35
|
+
const body = await new Promise<string>((resolve) => {
|
|
36
|
+
let data = "";
|
|
37
|
+
req.on("data", (chunk) => (data += chunk));
|
|
38
|
+
req.on("end", () => resolve(data));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const request = new Request(`https://${req.headers.host}${req.url}`, {
|
|
42
|
+
method: req.method || "POST",
|
|
43
|
+
headers: new Headers(req.headers as Record<string, string>),
|
|
44
|
+
body,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const response = await handler(request);
|
|
48
|
+
res.status(response.status).send(await response.text());
|
|
49
|
+
}
|