@better-auth/stripe 1.2.1-beta.2 → 1.2.1-beta.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.
@@ -1,17 +1,17 @@
1
1
 
2
- > @better-auth/stripe@1.2.1-beta.2 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.2.1-beta.4 build /home/runner/work/better-auth/better-auth/packages/stripe
3
3
  > unbuild
4
4
 
5
5
  [info] Automatically detected entries: src/index, src/client [esm] [cjs] [dts]
6
6
  [info] Building stripe
7
7
  [success] Build succeeded for stripe
8
- [log] dist/index.cjs (total size: 24.7 kB, chunk size: 24.7 kB, exports: stripe)
8
+ [log] dist/index.cjs (total size: 26 kB, chunk size: 26 kB, exports: stripe)
9
9
 
10
10
  [log] dist/client.cjs (total size: 160 B, chunk size: 160 B, exports: stripeClient)
11
11
 
12
- [log] dist/index.mjs (total size: 24.5 kB, chunk size: 24.5 kB, exports: stripe)
12
+ [log] dist/index.mjs (total size: 25.8 kB, chunk size: 25.8 kB, exports: stripe)
13
13
 
14
14
  [log] dist/client.mjs (total size: 133 B, chunk size: 133 B, exports: stripeClient)
15
15
 
16
- Σ Total dist size (byte size): 156 kB
16
+ Σ Total dist size (byte size): 159 kB
17
17
  [log]
package/dist/index.cjs CHANGED
@@ -4,6 +4,7 @@ const plugins = require('better-auth/plugins');
4
4
  const zod = require('zod');
5
5
  const api = require('better-auth/api');
6
6
  const crypto = require('better-auth/crypto');
7
+ const betterAuth = require('better-auth');
7
8
 
8
9
  async function getPlans(options) {
9
10
  return typeof options?.subscription?.plans === "function" ? await options.subscription?.plans() : options.subscription?.plans;
@@ -20,46 +21,37 @@ async function getPlanByName(options, name) {
20
21
  }
21
22
 
22
23
  async function onCheckoutSessionCompleted(ctx, options, event) {
23
- const client = options.stripeClient;
24
- const checkoutSession = event.data.object;
25
- if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
26
- return;
27
- }
28
- const subscription = await client.subscriptions.retrieve(
29
- checkoutSession.subscription
30
- );
31
- const priceId = subscription.items.data[0]?.price.id;
32
- const plan = await getPlanByPriceId(options, priceId);
33
- if (plan) {
34
- const referenceId = checkoutSession?.metadata?.referenceId;
35
- const subscriptionId = checkoutSession?.metadata?.subscriptionId;
36
- const seats = subscription.items.data[0].quantity;
37
- if (referenceId && subscriptionId) {
38
- const trial = subscription.trial_start && subscription.trial_end ? {
39
- trialStart: new Date(subscription.trial_start * 1e3),
40
- trialEnd: new Date(subscription.trial_end * 1e3)
41
- } : {};
42
- let dbSubscription = await ctx.context.adapter.update({
43
- model: "subscription",
44
- update: {
45
- plan: plan.name.toLowerCase(),
46
- status: subscription.status,
47
- updatedAt: /* @__PURE__ */ new Date(),
48
- periodStart: new Date(subscription.current_period_start * 1e3),
49
- periodEnd: new Date(subscription.current_period_end * 1e3),
50
- seats,
51
- ...trial
52
- },
53
- where: [
54
- {
55
- field: "id",
56
- value: subscriptionId
57
- }
58
- ]
59
- });
60
- if (!dbSubscription) {
61
- dbSubscription = await ctx.context.adapter.findOne({
24
+ try {
25
+ const client = options.stripeClient;
26
+ const checkoutSession = event.data.object;
27
+ if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
28
+ return;
29
+ }
30
+ const subscription = await client.subscriptions.retrieve(
31
+ checkoutSession.subscription
32
+ );
33
+ const priceId = subscription.items.data[0]?.price.id;
34
+ const plan = await getPlanByPriceId(options, priceId);
35
+ if (plan) {
36
+ const referenceId = checkoutSession?.metadata?.referenceId;
37
+ const subscriptionId = checkoutSession?.metadata?.subscriptionId;
38
+ const seats = subscription.items.data[0].quantity;
39
+ if (referenceId && subscriptionId) {
40
+ const trial = subscription.trial_start && subscription.trial_end ? {
41
+ trialStart: new Date(subscription.trial_start * 1e3),
42
+ trialEnd: new Date(subscription.trial_end * 1e3)
43
+ } : {};
44
+ let dbSubscription = await ctx.context.adapter.update({
62
45
  model: "subscription",
46
+ update: {
47
+ plan: plan.name.toLowerCase(),
48
+ status: subscription.status,
49
+ updatedAt: /* @__PURE__ */ new Date(),
50
+ periodStart: new Date(subscription.current_period_start * 1e3),
51
+ periodEnd: new Date(subscription.current_period_end * 1e3),
52
+ seats,
53
+ ...trial
54
+ },
63
55
  where: [
64
56
  {
65
57
  field: "id",
@@ -67,111 +59,146 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
67
59
  }
68
60
  ]
69
61
  });
62
+ if (trial.trialStart && plan.freeTrial?.onTrialStart) {
63
+ await plan.freeTrial.onTrialStart(dbSubscription);
64
+ }
65
+ if (!dbSubscription) {
66
+ dbSubscription = await ctx.context.adapter.findOne({
67
+ model: "subscription",
68
+ where: [
69
+ {
70
+ field: "id",
71
+ value: subscriptionId
72
+ }
73
+ ]
74
+ });
75
+ }
76
+ await options.subscription?.onSubscriptionComplete?.({
77
+ event,
78
+ subscription: dbSubscription,
79
+ stripeSubscription: subscription,
80
+ plan
81
+ });
82
+ return;
70
83
  }
71
- await options.subscription?.onSubscriptionComplete?.({
72
- event,
73
- subscription: dbSubscription,
74
- stripeSubscription: subscription,
75
- plan
76
- });
77
- return;
78
84
  }
85
+ } catch (e) {
86
+ betterAuth.logger.error(`Stripe webhook failed. Error: ${e.message}`);
79
87
  }
80
88
  }
81
89
  async function onSubscriptionUpdated(ctx, options, event) {
82
- if (!options.subscription?.enabled) {
83
- return;
84
- }
85
- const subscriptionUpdated = event.data.object;
86
- const priceId = subscriptionUpdated.items.data[0].price.id;
87
- const plan = await getPlanByPriceId(options, priceId);
88
- if (plan) {
89
- const stripeId = subscriptionUpdated.customer.toString();
90
- const subscription = await ctx.context.adapter.findOne({
91
- model: "subscription",
92
- where: [
93
- {
94
- field: "stripeSubscriptionId",
95
- value: stripeId
96
- }
97
- ]
98
- });
99
- if (!subscription) {
90
+ try {
91
+ if (!options.subscription?.enabled) {
100
92
  return;
101
93
  }
102
- const seats = subscriptionUpdated.items.data[0].quantity;
103
- await ctx.context.adapter.update({
104
- model: "subscription",
105
- update: {
106
- plan: plan.name.toLowerCase(),
107
- limits: plan.limits,
108
- updatedAt: /* @__PURE__ */ new Date(),
109
- status: subscriptionUpdated.status,
110
- periodStart: new Date(subscriptionUpdated.current_period_start * 1e3),
111
- periodEnd: new Date(subscriptionUpdated.current_period_end * 1e3),
112
- cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
113
- seats
114
- },
115
- where: [
116
- {
117
- field: "stripeSubscriptionId",
118
- value: subscriptionUpdated.id
119
- }
120
- ]
121
- });
122
- const subscriptionCanceled = subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end;
123
- if (subscriptionCanceled) {
124
- await options.subscription.onSubscriptionCancel?.({
125
- subscription,
126
- cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
127
- stripeSubscription: subscriptionUpdated,
128
- event
94
+ const subscriptionUpdated = event.data.object;
95
+ const priceId = subscriptionUpdated.items.data[0].price.id;
96
+ const plan = await getPlanByPriceId(options, priceId);
97
+ if (plan) {
98
+ const stripeId = subscriptionUpdated.id;
99
+ const subscription = await ctx.context.adapter.findOne({
100
+ model: "subscription",
101
+ where: [
102
+ {
103
+ field: "stripeSubscriptionId",
104
+ value: stripeId
105
+ }
106
+ ]
129
107
  });
108
+ if (!subscription) {
109
+ return;
110
+ }
111
+ const seats = subscriptionUpdated.items.data[0].quantity;
112
+ await ctx.context.adapter.update({
113
+ model: "subscription",
114
+ update: {
115
+ plan: plan.name.toLowerCase(),
116
+ limits: plan.limits,
117
+ updatedAt: /* @__PURE__ */ new Date(),
118
+ status: subscriptionUpdated.status,
119
+ periodStart: new Date(
120
+ subscriptionUpdated.current_period_start * 1e3
121
+ ),
122
+ periodEnd: new Date(subscriptionUpdated.current_period_end * 1e3),
123
+ cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
124
+ seats
125
+ },
126
+ where: [
127
+ {
128
+ field: "stripeSubscriptionId",
129
+ value: subscriptionUpdated.id
130
+ }
131
+ ]
132
+ });
133
+ const subscriptionCanceled = subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end;
134
+ if (subscriptionCanceled) {
135
+ await options.subscription.onSubscriptionCancel?.({
136
+ subscription,
137
+ cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
138
+ stripeSubscription: subscriptionUpdated,
139
+ event
140
+ });
141
+ }
142
+ await options.subscription.onSubscriptionUpdate?.({
143
+ event,
144
+ subscription
145
+ });
146
+ if (subscriptionUpdated.status === "active" && subscription.status === "trialing" && plan.freeTrial?.onTrialEnd) {
147
+ const user = await ctx.context.adapter.findOne({
148
+ model: "user",
149
+ where: [{ field: "id", value: subscription.referenceId }]
150
+ });
151
+ if (user) {
152
+ await plan.freeTrial.onTrialEnd({ subscription, user }, ctx.request);
153
+ }
154
+ }
155
+ if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) {
156
+ await plan.freeTrial.onTrialExpired(subscription, ctx.request);
157
+ }
130
158
  }
131
- await options.subscription.onSubscriptionUpdate?.({
132
- event,
133
- subscription
134
- });
159
+ } catch (error) {
160
+ betterAuth.logger.error(`Stripe webhook failed. Error: ${error.message}`);
135
161
  }
136
162
  }
137
163
  async function onSubscriptionDeleted(ctx, options, event) {
138
164
  if (!options.subscription?.enabled) {
139
165
  return;
140
166
  }
141
- const subscriptionDeleted = event.data.object;
142
- const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
143
- const stripeSubscription = await options.stripeClient.subscriptions.retrieve(
144
- subscriptionId
145
- );
146
- if (stripeSubscription.status === "canceled") {
147
- const subscription = await ctx.context.adapter.findOne({
148
- model: "subscription",
149
- where: [
150
- {
151
- field: "id",
152
- value: subscriptionId
153
- }
154
- ]
155
- });
156
- if (subscription) {
157
- await ctx.context.adapter.update({
167
+ try {
168
+ const subscriptionDeleted = event.data.object;
169
+ const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
170
+ if (subscriptionDeleted.status === "canceled") {
171
+ const subscription = await ctx.context.adapter.findOne({
158
172
  model: "subscription",
159
173
  where: [
160
174
  {
161
175
  field: "id",
162
- value: subscription.id
176
+ value: subscriptionId
163
177
  }
164
- ],
165
- update: {
166
- status: "canceled"
167
- }
168
- });
169
- await options.subscription.onSubscriptionDeleted?.({
170
- event,
171
- stripeSubscription: subscriptionDeleted,
172
- subscription
178
+ ]
173
179
  });
180
+ if (subscription) {
181
+ await ctx.context.adapter.update({
182
+ model: "subscription",
183
+ where: [
184
+ {
185
+ field: "id",
186
+ value: subscription.id
187
+ }
188
+ ],
189
+ update: {
190
+ status: "canceled"
191
+ }
192
+ });
193
+ await options.subscription.onSubscriptionDeleted?.({
194
+ event,
195
+ stripeSubscription: subscriptionDeleted,
196
+ subscription
197
+ });
198
+ }
174
199
  }
200
+ } catch (error) {
201
+ betterAuth.logger.error(`Stripe webhook failed. Error: ${error.message}`);
175
202
  }
176
203
  }
177
204
 
package/dist/index.d.cts CHANGED
@@ -78,7 +78,7 @@ type Plan = {
78
78
  * @param subscription - Subscription
79
79
  * @returns
80
80
  */
81
- onTrialExpired?: (subscription: Subscription) => Promise<void>;
81
+ onTrialExpired?: (subscription: Subscription, request?: Request) => Promise<void>;
82
82
  };
83
83
  };
84
84
  interface Subscription {
package/dist/index.d.mts CHANGED
@@ -78,7 +78,7 @@ type Plan = {
78
78
  * @param subscription - Subscription
79
79
  * @returns
80
80
  */
81
- onTrialExpired?: (subscription: Subscription) => Promise<void>;
81
+ onTrialExpired?: (subscription: Subscription, request?: Request) => Promise<void>;
82
82
  };
83
83
  };
84
84
  interface Subscription {
package/dist/index.d.ts CHANGED
@@ -78,7 +78,7 @@ type Plan = {
78
78
  * @param subscription - Subscription
79
79
  * @returns
80
80
  */
81
- onTrialExpired?: (subscription: Subscription) => Promise<void>;
81
+ onTrialExpired?: (subscription: Subscription, request?: Request) => Promise<void>;
82
82
  };
83
83
  };
84
84
  interface Subscription {
package/dist/index.mjs CHANGED
@@ -2,6 +2,7 @@ import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
2
2
  import { z } from 'zod';
3
3
  import { getSessionFromCtx, sessionMiddleware, originCheck, APIError } from 'better-auth/api';
4
4
  import { generateRandomString } from 'better-auth/crypto';
5
+ import { logger } from 'better-auth';
5
6
 
6
7
  async function getPlans(options) {
7
8
  return typeof options?.subscription?.plans === "function" ? await options.subscription?.plans() : options.subscription?.plans;
@@ -18,46 +19,37 @@ async function getPlanByName(options, name) {
18
19
  }
19
20
 
20
21
  async function onCheckoutSessionCompleted(ctx, options, event) {
21
- const client = options.stripeClient;
22
- const checkoutSession = event.data.object;
23
- if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
24
- return;
25
- }
26
- const subscription = await client.subscriptions.retrieve(
27
- checkoutSession.subscription
28
- );
29
- const priceId = subscription.items.data[0]?.price.id;
30
- const plan = await getPlanByPriceId(options, priceId);
31
- if (plan) {
32
- const referenceId = checkoutSession?.metadata?.referenceId;
33
- const subscriptionId = checkoutSession?.metadata?.subscriptionId;
34
- const seats = subscription.items.data[0].quantity;
35
- if (referenceId && subscriptionId) {
36
- const trial = subscription.trial_start && subscription.trial_end ? {
37
- trialStart: new Date(subscription.trial_start * 1e3),
38
- trialEnd: new Date(subscription.trial_end * 1e3)
39
- } : {};
40
- let dbSubscription = await ctx.context.adapter.update({
41
- model: "subscription",
42
- update: {
43
- plan: plan.name.toLowerCase(),
44
- status: subscription.status,
45
- updatedAt: /* @__PURE__ */ new Date(),
46
- periodStart: new Date(subscription.current_period_start * 1e3),
47
- periodEnd: new Date(subscription.current_period_end * 1e3),
48
- seats,
49
- ...trial
50
- },
51
- where: [
52
- {
53
- field: "id",
54
- value: subscriptionId
55
- }
56
- ]
57
- });
58
- if (!dbSubscription) {
59
- dbSubscription = await ctx.context.adapter.findOne({
22
+ try {
23
+ const client = options.stripeClient;
24
+ const checkoutSession = event.data.object;
25
+ if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
26
+ return;
27
+ }
28
+ const subscription = await client.subscriptions.retrieve(
29
+ checkoutSession.subscription
30
+ );
31
+ const priceId = subscription.items.data[0]?.price.id;
32
+ const plan = await getPlanByPriceId(options, priceId);
33
+ if (plan) {
34
+ const referenceId = checkoutSession?.metadata?.referenceId;
35
+ const subscriptionId = checkoutSession?.metadata?.subscriptionId;
36
+ const seats = subscription.items.data[0].quantity;
37
+ if (referenceId && subscriptionId) {
38
+ const trial = subscription.trial_start && subscription.trial_end ? {
39
+ trialStart: new Date(subscription.trial_start * 1e3),
40
+ trialEnd: new Date(subscription.trial_end * 1e3)
41
+ } : {};
42
+ let dbSubscription = await ctx.context.adapter.update({
60
43
  model: "subscription",
44
+ update: {
45
+ plan: plan.name.toLowerCase(),
46
+ status: subscription.status,
47
+ updatedAt: /* @__PURE__ */ new Date(),
48
+ periodStart: new Date(subscription.current_period_start * 1e3),
49
+ periodEnd: new Date(subscription.current_period_end * 1e3),
50
+ seats,
51
+ ...trial
52
+ },
61
53
  where: [
62
54
  {
63
55
  field: "id",
@@ -65,111 +57,146 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
65
57
  }
66
58
  ]
67
59
  });
60
+ if (trial.trialStart && plan.freeTrial?.onTrialStart) {
61
+ await plan.freeTrial.onTrialStart(dbSubscription);
62
+ }
63
+ if (!dbSubscription) {
64
+ dbSubscription = await ctx.context.adapter.findOne({
65
+ model: "subscription",
66
+ where: [
67
+ {
68
+ field: "id",
69
+ value: subscriptionId
70
+ }
71
+ ]
72
+ });
73
+ }
74
+ await options.subscription?.onSubscriptionComplete?.({
75
+ event,
76
+ subscription: dbSubscription,
77
+ stripeSubscription: subscription,
78
+ plan
79
+ });
80
+ return;
68
81
  }
69
- await options.subscription?.onSubscriptionComplete?.({
70
- event,
71
- subscription: dbSubscription,
72
- stripeSubscription: subscription,
73
- plan
74
- });
75
- return;
76
82
  }
83
+ } catch (e) {
84
+ logger.error(`Stripe webhook failed. Error: ${e.message}`);
77
85
  }
78
86
  }
79
87
  async function onSubscriptionUpdated(ctx, options, event) {
80
- if (!options.subscription?.enabled) {
81
- return;
82
- }
83
- const subscriptionUpdated = event.data.object;
84
- const priceId = subscriptionUpdated.items.data[0].price.id;
85
- const plan = await getPlanByPriceId(options, priceId);
86
- if (plan) {
87
- const stripeId = subscriptionUpdated.customer.toString();
88
- const subscription = await ctx.context.adapter.findOne({
89
- model: "subscription",
90
- where: [
91
- {
92
- field: "stripeSubscriptionId",
93
- value: stripeId
94
- }
95
- ]
96
- });
97
- if (!subscription) {
88
+ try {
89
+ if (!options.subscription?.enabled) {
98
90
  return;
99
91
  }
100
- const seats = subscriptionUpdated.items.data[0].quantity;
101
- await ctx.context.adapter.update({
102
- model: "subscription",
103
- update: {
104
- plan: plan.name.toLowerCase(),
105
- limits: plan.limits,
106
- updatedAt: /* @__PURE__ */ new Date(),
107
- status: subscriptionUpdated.status,
108
- periodStart: new Date(subscriptionUpdated.current_period_start * 1e3),
109
- periodEnd: new Date(subscriptionUpdated.current_period_end * 1e3),
110
- cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
111
- seats
112
- },
113
- where: [
114
- {
115
- field: "stripeSubscriptionId",
116
- value: subscriptionUpdated.id
117
- }
118
- ]
119
- });
120
- const subscriptionCanceled = subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end;
121
- if (subscriptionCanceled) {
122
- await options.subscription.onSubscriptionCancel?.({
123
- subscription,
124
- cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
125
- stripeSubscription: subscriptionUpdated,
126
- event
92
+ const subscriptionUpdated = event.data.object;
93
+ const priceId = subscriptionUpdated.items.data[0].price.id;
94
+ const plan = await getPlanByPriceId(options, priceId);
95
+ if (plan) {
96
+ const stripeId = subscriptionUpdated.id;
97
+ const subscription = await ctx.context.adapter.findOne({
98
+ model: "subscription",
99
+ where: [
100
+ {
101
+ field: "stripeSubscriptionId",
102
+ value: stripeId
103
+ }
104
+ ]
127
105
  });
106
+ if (!subscription) {
107
+ return;
108
+ }
109
+ const seats = subscriptionUpdated.items.data[0].quantity;
110
+ await ctx.context.adapter.update({
111
+ model: "subscription",
112
+ update: {
113
+ plan: plan.name.toLowerCase(),
114
+ limits: plan.limits,
115
+ updatedAt: /* @__PURE__ */ new Date(),
116
+ status: subscriptionUpdated.status,
117
+ periodStart: new Date(
118
+ subscriptionUpdated.current_period_start * 1e3
119
+ ),
120
+ periodEnd: new Date(subscriptionUpdated.current_period_end * 1e3),
121
+ cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
122
+ seats
123
+ },
124
+ where: [
125
+ {
126
+ field: "stripeSubscriptionId",
127
+ value: subscriptionUpdated.id
128
+ }
129
+ ]
130
+ });
131
+ const subscriptionCanceled = subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end;
132
+ if (subscriptionCanceled) {
133
+ await options.subscription.onSubscriptionCancel?.({
134
+ subscription,
135
+ cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
136
+ stripeSubscription: subscriptionUpdated,
137
+ event
138
+ });
139
+ }
140
+ await options.subscription.onSubscriptionUpdate?.({
141
+ event,
142
+ subscription
143
+ });
144
+ if (subscriptionUpdated.status === "active" && subscription.status === "trialing" && plan.freeTrial?.onTrialEnd) {
145
+ const user = await ctx.context.adapter.findOne({
146
+ model: "user",
147
+ where: [{ field: "id", value: subscription.referenceId }]
148
+ });
149
+ if (user) {
150
+ await plan.freeTrial.onTrialEnd({ subscription, user }, ctx.request);
151
+ }
152
+ }
153
+ if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) {
154
+ await plan.freeTrial.onTrialExpired(subscription, ctx.request);
155
+ }
128
156
  }
129
- await options.subscription.onSubscriptionUpdate?.({
130
- event,
131
- subscription
132
- });
157
+ } catch (error) {
158
+ logger.error(`Stripe webhook failed. Error: ${error.message}`);
133
159
  }
134
160
  }
135
161
  async function onSubscriptionDeleted(ctx, options, event) {
136
162
  if (!options.subscription?.enabled) {
137
163
  return;
138
164
  }
139
- const subscriptionDeleted = event.data.object;
140
- const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
141
- const stripeSubscription = await options.stripeClient.subscriptions.retrieve(
142
- subscriptionId
143
- );
144
- if (stripeSubscription.status === "canceled") {
145
- const subscription = await ctx.context.adapter.findOne({
146
- model: "subscription",
147
- where: [
148
- {
149
- field: "id",
150
- value: subscriptionId
151
- }
152
- ]
153
- });
154
- if (subscription) {
155
- await ctx.context.adapter.update({
165
+ try {
166
+ const subscriptionDeleted = event.data.object;
167
+ const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
168
+ if (subscriptionDeleted.status === "canceled") {
169
+ const subscription = await ctx.context.adapter.findOne({
156
170
  model: "subscription",
157
171
  where: [
158
172
  {
159
173
  field: "id",
160
- value: subscription.id
174
+ value: subscriptionId
161
175
  }
162
- ],
163
- update: {
164
- status: "canceled"
165
- }
166
- });
167
- await options.subscription.onSubscriptionDeleted?.({
168
- event,
169
- stripeSubscription: subscriptionDeleted,
170
- subscription
176
+ ]
171
177
  });
178
+ if (subscription) {
179
+ await ctx.context.adapter.update({
180
+ model: "subscription",
181
+ where: [
182
+ {
183
+ field: "id",
184
+ value: subscription.id
185
+ }
186
+ ],
187
+ update: {
188
+ status: "canceled"
189
+ }
190
+ });
191
+ await options.subscription.onSubscriptionDeleted?.({
192
+ event,
193
+ stripeSubscription: subscriptionDeleted,
194
+ subscription
195
+ });
196
+ }
172
197
  }
198
+ } catch (error) {
199
+ logger.error(`Stripe webhook failed. Error: ${error.message}`);
173
200
  }
174
201
  }
175
202
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/stripe",
3
3
  "author": "Bereket Engida",
4
- "version": "1.2.1-beta.2",
4
+ "version": "1.2.1-beta.4",
5
5
  "main": "dist/index.cjs",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "zod": "^3.24.1",
38
- "better-auth": "^1.2.1-beta.2"
38
+ "better-auth": "^1.2.1-beta.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/better-sqlite3": "^7.6.12",
package/src/hooks.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GenericEndpointContext } from "better-auth";
1
+ import { logger, type GenericEndpointContext, type User } from "better-auth";
2
2
  import type Stripe from "stripe";
3
3
  import type { InputSubscription, StripeOptions, Subscription } from "./types";
4
4
  import { getPlanByPriceId } from "./utils";
@@ -8,65 +8,76 @@ export async function onCheckoutSessionCompleted(
8
8
  options: StripeOptions,
9
9
  event: Stripe.Event,
10
10
  ) {
11
- const client = options.stripeClient;
12
- const checkoutSession = event.data.object as Stripe.Checkout.Session;
13
- if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
14
- return;
15
- }
16
- const subscription = await client.subscriptions.retrieve(
17
- checkoutSession.subscription as string,
18
- );
19
- const priceId = subscription.items.data[0]?.price.id;
20
- const plan = await getPlanByPriceId(options, priceId as string);
21
- if (plan) {
22
- const referenceId = checkoutSession?.metadata?.referenceId;
23
- const subscriptionId = checkoutSession?.metadata?.subscriptionId;
24
- const seats = subscription.items.data[0].quantity;
25
- if (referenceId && subscriptionId) {
26
- const trial =
27
- subscription.trial_start && subscription.trial_end
28
- ? {
29
- trialStart: new Date(subscription.trial_start * 1000),
30
- trialEnd: new Date(subscription.trial_end * 1000),
31
- }
32
- : {};
33
- let dbSubscription = await ctx.context.adapter.update<InputSubscription>({
34
- model: "subscription",
35
- update: {
36
- plan: plan.name.toLowerCase(),
37
- status: subscription.status,
38
- updatedAt: new Date(),
39
- periodStart: new Date(subscription.current_period_start * 1000),
40
- periodEnd: new Date(subscription.current_period_end * 1000),
41
- seats,
42
- ...trial,
43
- },
44
- where: [
45
- {
46
- field: "id",
47
- value: subscriptionId,
48
- },
49
- ],
50
- });
51
- if (!dbSubscription) {
52
- dbSubscription = await ctx.context.adapter.findOne<Subscription>({
53
- model: "subscription",
54
- where: [
55
- {
56
- field: "id",
57
- value: subscriptionId,
11
+ try {
12
+ const client = options.stripeClient;
13
+ const checkoutSession = event.data.object as Stripe.Checkout.Session;
14
+ if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
15
+ return;
16
+ }
17
+ const subscription = await client.subscriptions.retrieve(
18
+ checkoutSession.subscription as string,
19
+ );
20
+ const priceId = subscription.items.data[0]?.price.id;
21
+ const plan = await getPlanByPriceId(options, priceId as string);
22
+ if (plan) {
23
+ const referenceId = checkoutSession?.metadata?.referenceId;
24
+ const subscriptionId = checkoutSession?.metadata?.subscriptionId;
25
+ const seats = subscription.items.data[0].quantity;
26
+ if (referenceId && subscriptionId) {
27
+ const trial =
28
+ subscription.trial_start && subscription.trial_end
29
+ ? {
30
+ trialStart: new Date(subscription.trial_start * 1000),
31
+ trialEnd: new Date(subscription.trial_end * 1000),
32
+ }
33
+ : {};
34
+
35
+ let dbSubscription =
36
+ await ctx.context.adapter.update<InputSubscription>({
37
+ model: "subscription",
38
+ update: {
39
+ plan: plan.name.toLowerCase(),
40
+ status: subscription.status,
41
+ updatedAt: new Date(),
42
+ periodStart: new Date(subscription.current_period_start * 1000),
43
+ periodEnd: new Date(subscription.current_period_end * 1000),
44
+ seats,
45
+ ...trial,
58
46
  },
59
- ],
47
+ where: [
48
+ {
49
+ field: "id",
50
+ value: subscriptionId,
51
+ },
52
+ ],
53
+ });
54
+
55
+ if (trial.trialStart && plan.freeTrial?.onTrialStart) {
56
+ await plan.freeTrial.onTrialStart(dbSubscription as Subscription);
57
+ }
58
+
59
+ if (!dbSubscription) {
60
+ dbSubscription = await ctx.context.adapter.findOne<Subscription>({
61
+ model: "subscription",
62
+ where: [
63
+ {
64
+ field: "id",
65
+ value: subscriptionId,
66
+ },
67
+ ],
68
+ });
69
+ }
70
+ await options.subscription?.onSubscriptionComplete?.({
71
+ event,
72
+ subscription: dbSubscription as Subscription,
73
+ stripeSubscription: subscription,
74
+ plan,
60
75
  });
76
+ return;
61
77
  }
62
- await options.subscription?.onSubscriptionComplete?.({
63
- event,
64
- subscription: dbSubscription as Subscription,
65
- stripeSubscription: subscription,
66
- plan,
67
- });
68
- return;
69
78
  }
79
+ } catch (e: any) {
80
+ logger.error(`Stripe webhook failed. Error: ${e.message}`);
70
81
  }
71
82
  }
72
83
 
@@ -75,62 +86,89 @@ export async function onSubscriptionUpdated(
75
86
  options: StripeOptions,
76
87
  event: Stripe.Event,
77
88
  ) {
78
- if (!options.subscription?.enabled) {
79
- return;
80
- }
81
- const subscriptionUpdated = event.data.object as Stripe.Subscription;
82
- const priceId = subscriptionUpdated.items.data[0].price.id;
83
- const plan = await getPlanByPriceId(options, priceId);
84
- if (plan) {
85
- const stripeId = subscriptionUpdated.customer.toString();
86
- const subscription = await ctx.context.adapter.findOne<Subscription>({
87
- model: "subscription",
88
- where: [
89
- {
90
- field: "stripeSubscriptionId",
91
- value: stripeId,
92
- },
93
- ],
94
- });
95
- if (!subscription) {
89
+ try {
90
+ if (!options.subscription?.enabled) {
96
91
  return;
97
92
  }
98
- const seats = subscriptionUpdated.items.data[0].quantity;
99
- await ctx.context.adapter.update({
100
- model: "subscription",
101
- update: {
102
- plan: plan.name.toLowerCase(),
103
- limits: plan.limits,
104
- updatedAt: new Date(),
105
- status: subscriptionUpdated.status,
106
- periodStart: new Date(subscriptionUpdated.current_period_start * 1000),
107
- periodEnd: new Date(subscriptionUpdated.current_period_end * 1000),
108
- cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
109
- seats,
110
- },
111
- where: [
112
- {
113
- field: "stripeSubscriptionId",
114
- value: subscriptionUpdated.id,
93
+ const subscriptionUpdated = event.data.object as Stripe.Subscription;
94
+ const priceId = subscriptionUpdated.items.data[0].price.id;
95
+ const plan = await getPlanByPriceId(options, priceId);
96
+ if (plan) {
97
+ const stripeId = subscriptionUpdated.id;
98
+ const subscription = await ctx.context.adapter.findOne<Subscription>({
99
+ model: "subscription",
100
+ where: [
101
+ {
102
+ field: "stripeSubscriptionId",
103
+ value: stripeId,
104
+ },
105
+ ],
106
+ });
107
+ if (!subscription) {
108
+ return;
109
+ }
110
+ const seats = subscriptionUpdated.items.data[0].quantity;
111
+ await ctx.context.adapter.update({
112
+ model: "subscription",
113
+ update: {
114
+ plan: plan.name.toLowerCase(),
115
+ limits: plan.limits,
116
+ updatedAt: new Date(),
117
+ status: subscriptionUpdated.status,
118
+ periodStart: new Date(
119
+ subscriptionUpdated.current_period_start * 1000,
120
+ ),
121
+ periodEnd: new Date(subscriptionUpdated.current_period_end * 1000),
122
+ cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
123
+ seats,
115
124
  },
116
- ],
117
- });
118
- const subscriptionCanceled =
119
- subscriptionUpdated.status === "active" &&
120
- subscriptionUpdated.cancel_at_period_end;
121
- if (subscriptionCanceled) {
122
- await options.subscription.onSubscriptionCancel?.({
123
- subscription,
124
- cancellationDetails:
125
- subscriptionUpdated.cancellation_details || undefined,
126
- stripeSubscription: subscriptionUpdated,
125
+ where: [
126
+ {
127
+ field: "stripeSubscriptionId",
128
+ value: subscriptionUpdated.id,
129
+ },
130
+ ],
131
+ });
132
+ const subscriptionCanceled =
133
+ subscriptionUpdated.status === "active" &&
134
+ subscriptionUpdated.cancel_at_period_end;
135
+ if (subscriptionCanceled) {
136
+ await options.subscription.onSubscriptionCancel?.({
137
+ subscription,
138
+ cancellationDetails:
139
+ subscriptionUpdated.cancellation_details || undefined,
140
+ stripeSubscription: subscriptionUpdated,
141
+ event,
142
+ });
143
+ }
144
+ await options.subscription.onSubscriptionUpdate?.({
127
145
  event,
146
+ subscription,
128
147
  });
148
+
149
+ if (
150
+ subscriptionUpdated.status === "active" &&
151
+ subscription.status === "trialing" &&
152
+ plan.freeTrial?.onTrialEnd
153
+ ) {
154
+ const user = await ctx.context.adapter.findOne<User>({
155
+ model: "user",
156
+ where: [{ field: "id", value: subscription.referenceId }],
157
+ });
158
+ if (user) {
159
+ await plan.freeTrial.onTrialEnd({ subscription, user }, ctx.request);
160
+ }
161
+ }
162
+ if (
163
+ subscriptionUpdated.status === "incomplete_expired" &&
164
+ subscription.status === "trialing" &&
165
+ plan.freeTrial?.onTrialExpired
166
+ ) {
167
+ await plan.freeTrial.onTrialExpired(subscription, ctx.request);
168
+ }
129
169
  }
130
- await options.subscription.onSubscriptionUpdate?.({
131
- event,
132
- subscription,
133
- });
170
+ } catch (error: any) {
171
+ logger.error(`Stripe webhook failed. Error: ${error.message}`);
134
172
  }
135
173
  }
136
174
 
@@ -142,39 +180,40 @@ export async function onSubscriptionDeleted(
142
180
  if (!options.subscription?.enabled) {
143
181
  return;
144
182
  }
145
- const subscriptionDeleted = event.data.object as Stripe.Subscription;
146
- const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
147
- const stripeSubscription = await options.stripeClient.subscriptions.retrieve(
148
- subscriptionId as string,
149
- );
150
- if (stripeSubscription.status === "canceled") {
151
- const subscription = await ctx.context.adapter.findOne<Subscription>({
152
- model: "subscription",
153
- where: [
154
- {
155
- field: "id",
156
- value: subscriptionId,
157
- },
158
- ],
159
- });
160
- if (subscription) {
161
- await ctx.context.adapter.update({
183
+ try {
184
+ const subscriptionDeleted = event.data.object as Stripe.Subscription;
185
+ const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
186
+ if (subscriptionDeleted.status === "canceled") {
187
+ const subscription = await ctx.context.adapter.findOne<Subscription>({
162
188
  model: "subscription",
163
189
  where: [
164
190
  {
165
191
  field: "id",
166
- value: subscription.id,
192
+ value: subscriptionId,
167
193
  },
168
194
  ],
169
- update: {
170
- status: "canceled",
171
- },
172
- });
173
- await options.subscription.onSubscriptionDeleted?.({
174
- event,
175
- stripeSubscription: subscriptionDeleted,
176
- subscription,
177
195
  });
196
+ if (subscription) {
197
+ await ctx.context.adapter.update({
198
+ model: "subscription",
199
+ where: [
200
+ {
201
+ field: "id",
202
+ value: subscription.id,
203
+ },
204
+ ],
205
+ update: {
206
+ status: "canceled",
207
+ },
208
+ });
209
+ await options.subscription.onSubscriptionDeleted?.({
210
+ event,
211
+ stripeSubscription: subscriptionDeleted,
212
+ subscription,
213
+ });
214
+ }
178
215
  }
216
+ } catch (error: any) {
217
+ logger.error(`Stripe webhook failed. Error: ${error.message}`);
179
218
  }
180
219
  }
@@ -453,4 +453,232 @@ describe("stripe", async () => {
453
453
  expect(updatedSubscription?.status).toBe("canceled");
454
454
  }
455
455
  });
456
+
457
+ it("should execute subscription event handlers", async () => {
458
+ const onSubscriptionComplete = vi.fn();
459
+ const onSubscriptionUpdate = vi.fn();
460
+ const onSubscriptionCancel = vi.fn();
461
+ const onSubscriptionDeleted = vi.fn();
462
+
463
+ const testOptions = {
464
+ ...stripeOptions,
465
+ subscription: {
466
+ ...stripeOptions.subscription,
467
+ onSubscriptionComplete,
468
+ onSubscriptionUpdate,
469
+ onSubscriptionCancel,
470
+ onSubscriptionDeleted,
471
+ },
472
+ stripeWebhookSecret: "test_secret",
473
+ };
474
+
475
+ const testAuth = betterAuth({
476
+ baseURL: "http://localhost:3000",
477
+ database: memory,
478
+ emailAndPassword: {
479
+ enabled: true,
480
+ },
481
+ plugins: [stripe(testOptions)],
482
+ });
483
+
484
+ // Test subscription complete handler
485
+ const completeEvent = {
486
+ type: "checkout.session.completed",
487
+ data: {
488
+ object: {
489
+ mode: "subscription",
490
+ subscription: "sub_123",
491
+ metadata: {
492
+ referenceId: "user_123",
493
+ subscriptionId: "sub_123",
494
+ },
495
+ },
496
+ },
497
+ };
498
+
499
+ const mockSubscription = {
500
+ id: "sub_123",
501
+ status: "active",
502
+ items: {
503
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
504
+ },
505
+ current_period_start: Math.floor(Date.now() / 1000),
506
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
507
+ };
508
+
509
+ const mockStripeForEvents = {
510
+ ...testOptions.stripeClient,
511
+ subscriptions: {
512
+ retrieve: vi.fn().mockResolvedValue(mockSubscription),
513
+ },
514
+ webhooks: {
515
+ constructEvent: vi.fn().mockReturnValue(completeEvent),
516
+ },
517
+ };
518
+
519
+ const eventTestOptions = {
520
+ ...testOptions,
521
+ stripeClient: mockStripeForEvents as unknown as Stripe,
522
+ };
523
+
524
+ const eventTestAuth = betterAuth({
525
+ baseURL: "http://localhost:3000",
526
+ database: memory,
527
+ emailAndPassword: { enabled: true },
528
+ plugins: [stripe(eventTestOptions)],
529
+ });
530
+
531
+ await ctx.adapter.create({
532
+ model: "subscription",
533
+ data: {
534
+ id: "sub_123",
535
+ referenceId: "user_123",
536
+ stripeCustomerId: "cus_123",
537
+ stripeSubscriptionId: "sub_123",
538
+ status: "incomplete",
539
+ plan: "starter",
540
+ },
541
+ });
542
+
543
+ const webhookRequest = new Request(
544
+ "http://localhost:3000/api/auth/stripe/webhook",
545
+ {
546
+ method: "POST",
547
+ headers: {
548
+ "stripe-signature": "test_signature",
549
+ },
550
+ body: JSON.stringify(completeEvent),
551
+ },
552
+ );
553
+
554
+ await eventTestAuth.handler(webhookRequest);
555
+
556
+ expect(onSubscriptionComplete).toHaveBeenCalledWith(
557
+ expect.objectContaining({
558
+ event: expect.any(Object),
559
+ subscription: expect.any(Object),
560
+ stripeSubscription: expect.any(Object),
561
+ plan: expect.any(Object),
562
+ }),
563
+ );
564
+
565
+ const updateEvent = {
566
+ type: "customer.subscription.updated",
567
+ data: {
568
+ object: {
569
+ id: "sub_123",
570
+ customer: "cus_123",
571
+ status: "active",
572
+ items: {
573
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
574
+ },
575
+ current_period_start: Math.floor(Date.now() / 1000),
576
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
577
+ },
578
+ },
579
+ };
580
+
581
+ const updateRequest = new Request(
582
+ "http://localhost:3000/api/auth/stripe/webhook",
583
+ {
584
+ method: "POST",
585
+ headers: {
586
+ "stripe-signature": "test_signature",
587
+ },
588
+ body: JSON.stringify(updateEvent),
589
+ },
590
+ );
591
+
592
+ mockStripeForEvents.webhooks.constructEvent.mockReturnValue(updateEvent);
593
+ await eventTestAuth.handler(updateRequest);
594
+
595
+ expect(onSubscriptionUpdate).toHaveBeenCalledWith(
596
+ expect.objectContaining({
597
+ event: expect.any(Object),
598
+ subscription: expect.any(Object),
599
+ }),
600
+ );
601
+
602
+ const cancelEvent = {
603
+ type: "customer.subscription.updated",
604
+ data: {
605
+ object: {
606
+ id: "sub_123",
607
+ customer: "cus_123",
608
+ status: "active",
609
+ cancel_at_period_end: true,
610
+ items: {
611
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
612
+ },
613
+ current_period_start: Math.floor(Date.now() / 1000),
614
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
615
+ },
616
+ },
617
+ };
618
+
619
+ const cancelRequest = new Request(
620
+ "http://localhost:3000/api/auth/stripe/webhook",
621
+ {
622
+ method: "POST",
623
+ headers: {
624
+ "stripe-signature": "test_signature",
625
+ },
626
+ body: JSON.stringify(cancelEvent),
627
+ },
628
+ );
629
+
630
+ mockStripeForEvents.webhooks.constructEvent.mockReturnValue(cancelEvent);
631
+ await eventTestAuth.handler(cancelRequest);
632
+
633
+ expect(onSubscriptionCancel).toHaveBeenCalledWith({
634
+ event: cancelEvent,
635
+ subscription: expect.objectContaining({
636
+ id: "sub_123",
637
+ }),
638
+ stripeSubscription: expect.objectContaining({
639
+ id: "sub_123",
640
+ cancel_at_period_end: true,
641
+ }),
642
+ });
643
+
644
+ const deleteEvent = {
645
+ type: "customer.subscription.deleted",
646
+ data: {
647
+ object: {
648
+ id: "sub_123",
649
+ customer: "cus_123",
650
+ status: "canceled",
651
+ metadata: {
652
+ referenceId: "user_123",
653
+ subscriptionId: "sub_123",
654
+ },
655
+ },
656
+ },
657
+ };
658
+
659
+ const deleteRequest = new Request(
660
+ "http://localhost:3000/api/auth/stripe/webhook",
661
+ {
662
+ method: "POST",
663
+ headers: {
664
+ "stripe-signature": "test_signature",
665
+ },
666
+ body: JSON.stringify(deleteEvent),
667
+ },
668
+ );
669
+
670
+ mockStripeForEvents.webhooks.constructEvent.mockReturnValue(deleteEvent);
671
+ await eventTestAuth.handler(deleteRequest);
672
+
673
+ expect(onSubscriptionDeleted).toHaveBeenCalledWith({
674
+ event: deleteEvent,
675
+ subscription: expect.objectContaining({
676
+ id: "sub_123",
677
+ }),
678
+ stripeSubscription: expect.objectContaining({
679
+ id: "sub_123",
680
+ status: "canceled",
681
+ }),
682
+ });
683
+ });
456
684
  });
package/src/types.ts CHANGED
@@ -77,7 +77,10 @@ export type Plan = {
77
77
  * @param subscription - Subscription
78
78
  * @returns
79
79
  */
80
- onTrialExpired?: (subscription: Subscription) => Promise<void>;
80
+ onTrialExpired?: (
81
+ subscription: Subscription,
82
+ request?: Request,
83
+ ) => Promise<void>;
81
84
  };
82
85
  };
83
86