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/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 createStripeWebhookHandler(config) {
4
+ function createStripeHandler(config = {}) {
5
5
  const {
6
- databaseUrl,
7
- stripeSecretKey,
8
- stripeWebhookSecret,
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 sync = new StripeSync({
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
- const stripe = new Stripe(stripeSecretKey);
21
- return async function handler(request) {
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
- await sync.processWebhook(body, signature);
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
- createStripeWebhookHandler
206
+ createStripeHandler
81
207
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stripe-no-webhooks",
3
- "version": "0.0.2",
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
- "publish": "npm version patch && npm publish"
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
+ }