@better-auth/stripe 1.2.1-beta.3 → 1.2.1-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,17 @@
1
1
 
2
- > @better-auth/stripe@1.2.1-beta.3 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.2.1-beta.5 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.1 kB, chunk size: 26.1 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.9 kB, chunk size: 25.9 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,147 @@ 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
+ ]
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
129
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.id;
170
+ if (subscriptionDeleted.status === "canceled") {
171
+ const subscription = await ctx.context.adapter.findOne({
158
172
  model: "subscription",
159
173
  where: [
160
174
  {
161
- field: "id",
162
- value: subscription.id
175
+ field: "stripeSubscriptionId",
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: "stripeSubscriptionId",
186
+ value: subscriptionId
187
+ }
188
+ ],
189
+ update: {
190
+ status: "canceled",
191
+ updatedAt: /* @__PURE__ */ new Date()
192
+ }
193
+ });
194
+ await options.subscription.onSubscriptionDeleted?.({
195
+ event,
196
+ stripeSubscription: subscriptionDeleted,
197
+ subscription
198
+ });
199
+ }
174
200
  }
201
+ } catch (error) {
202
+ betterAuth.logger.error(`Stripe webhook failed. Error: ${error.message}`);
175
203
  }
176
204
  }
177
205
 
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,147 @@ 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
+ ]
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
127
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.id;
168
+ if (subscriptionDeleted.status === "canceled") {
169
+ const subscription = await ctx.context.adapter.findOne({
156
170
  model: "subscription",
157
171
  where: [
158
172
  {
159
- field: "id",
160
- value: subscription.id
173
+ field: "stripeSubscriptionId",
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: "stripeSubscriptionId",
184
+ value: subscriptionId
185
+ }
186
+ ],
187
+ update: {
188
+ status: "canceled",
189
+ updatedAt: /* @__PURE__ */ new Date()
190
+ }
191
+ });
192
+ await options.subscription.onSubscriptionDeleted?.({
193
+ event,
194
+ stripeSubscription: subscriptionDeleted,
195
+ subscription
196
+ });
197
+ }
172
198
  }
199
+ } catch (error) {
200
+ logger.error(`Stripe webhook failed. Error: ${error.message}`);
173
201
  }
174
202
  }
175
203
 
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.3",
4
+ "version": "1.2.1-beta.5",
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.3"
38
+ "better-auth": "^1.2.1-beta.5"
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,41 @@ 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.id;
186
+ if (subscriptionDeleted.status === "canceled") {
187
+ const subscription = await ctx.context.adapter.findOne<Subscription>({
162
188
  model: "subscription",
163
189
  where: [
164
190
  {
165
- field: "id",
166
- value: subscription.id,
191
+ field: "stripeSubscriptionId",
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: "stripeSubscriptionId",
202
+ value: subscriptionId,
203
+ },
204
+ ],
205
+ update: {
206
+ status: "canceled",
207
+ updatedAt: new Date(),
208
+ },
209
+ });
210
+ await options.subscription.onSubscriptionDeleted?.({
211
+ event,
212
+ stripeSubscription: subscriptionDeleted,
213
+ subscription,
214
+ });
215
+ }
178
216
  }
217
+ } catch (error: any) {
218
+ logger.error(`Stripe webhook failed. Error: ${error.message}`);
179
219
  }
180
220
  }
@@ -370,6 +370,7 @@ describe("stripe", async () => {
370
370
  stripeCustomerId: "cus_delete_test",
371
371
  status: "active",
372
372
  plan: "starter",
373
+ stripeSubscriptionId: "sub_delete_test",
373
374
  },
374
375
  });
375
376
 
@@ -387,7 +388,7 @@ describe("stripe", async () => {
387
388
  type: "customer.subscription.deleted",
388
389
  data: {
389
390
  object: {
390
- id: "sub_deleted",
391
+ id: "sub_delete_test",
391
392
  customer: subscription?.stripeCustomerId,
392
393
  status: "canceled",
393
394
  metadata: {
@@ -453,4 +454,232 @@ describe("stripe", async () => {
453
454
  expect(updatedSubscription?.status).toBe("canceled");
454
455
  }
455
456
  });
457
+
458
+ it("should execute subscription event handlers", async () => {
459
+ const onSubscriptionComplete = vi.fn();
460
+ const onSubscriptionUpdate = vi.fn();
461
+ const onSubscriptionCancel = vi.fn();
462
+ const onSubscriptionDeleted = vi.fn();
463
+
464
+ const testOptions = {
465
+ ...stripeOptions,
466
+ subscription: {
467
+ ...stripeOptions.subscription,
468
+ onSubscriptionComplete,
469
+ onSubscriptionUpdate,
470
+ onSubscriptionCancel,
471
+ onSubscriptionDeleted,
472
+ },
473
+ stripeWebhookSecret: "test_secret",
474
+ };
475
+
476
+ const testAuth = betterAuth({
477
+ baseURL: "http://localhost:3000",
478
+ database: memory,
479
+ emailAndPassword: {
480
+ enabled: true,
481
+ },
482
+ plugins: [stripe(testOptions)],
483
+ });
484
+
485
+ // Test subscription complete handler
486
+ const completeEvent = {
487
+ type: "checkout.session.completed",
488
+ data: {
489
+ object: {
490
+ mode: "subscription",
491
+ subscription: "sub_123",
492
+ metadata: {
493
+ referenceId: "user_123",
494
+ subscriptionId: "sub_123",
495
+ },
496
+ },
497
+ },
498
+ };
499
+
500
+ const mockSubscription = {
501
+ id: "sub_123",
502
+ status: "active",
503
+ items: {
504
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
505
+ },
506
+ current_period_start: Math.floor(Date.now() / 1000),
507
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
508
+ };
509
+
510
+ const mockStripeForEvents = {
511
+ ...testOptions.stripeClient,
512
+ subscriptions: {
513
+ retrieve: vi.fn().mockResolvedValue(mockSubscription),
514
+ },
515
+ webhooks: {
516
+ constructEvent: vi.fn().mockReturnValue(completeEvent),
517
+ },
518
+ };
519
+
520
+ const eventTestOptions = {
521
+ ...testOptions,
522
+ stripeClient: mockStripeForEvents as unknown as Stripe,
523
+ };
524
+
525
+ const eventTestAuth = betterAuth({
526
+ baseURL: "http://localhost:3000",
527
+ database: memory,
528
+ emailAndPassword: { enabled: true },
529
+ plugins: [stripe(eventTestOptions)],
530
+ });
531
+
532
+ await ctx.adapter.create({
533
+ model: "subscription",
534
+ data: {
535
+ id: "sub_123",
536
+ referenceId: "user_123",
537
+ stripeCustomerId: "cus_123",
538
+ stripeSubscriptionId: "sub_123",
539
+ status: "incomplete",
540
+ plan: "starter",
541
+ },
542
+ });
543
+
544
+ const webhookRequest = new Request(
545
+ "http://localhost:3000/api/auth/stripe/webhook",
546
+ {
547
+ method: "POST",
548
+ headers: {
549
+ "stripe-signature": "test_signature",
550
+ },
551
+ body: JSON.stringify(completeEvent),
552
+ },
553
+ );
554
+
555
+ await eventTestAuth.handler(webhookRequest);
556
+
557
+ expect(onSubscriptionComplete).toHaveBeenCalledWith(
558
+ expect.objectContaining({
559
+ event: expect.any(Object),
560
+ subscription: expect.any(Object),
561
+ stripeSubscription: expect.any(Object),
562
+ plan: expect.any(Object),
563
+ }),
564
+ );
565
+
566
+ const updateEvent = {
567
+ type: "customer.subscription.updated",
568
+ data: {
569
+ object: {
570
+ id: "sub_123",
571
+ customer: "cus_123",
572
+ status: "active",
573
+ items: {
574
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
575
+ },
576
+ current_period_start: Math.floor(Date.now() / 1000),
577
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
578
+ },
579
+ },
580
+ };
581
+
582
+ const updateRequest = new Request(
583
+ "http://localhost:3000/api/auth/stripe/webhook",
584
+ {
585
+ method: "POST",
586
+ headers: {
587
+ "stripe-signature": "test_signature",
588
+ },
589
+ body: JSON.stringify(updateEvent),
590
+ },
591
+ );
592
+
593
+ mockStripeForEvents.webhooks.constructEvent.mockReturnValue(updateEvent);
594
+ await eventTestAuth.handler(updateRequest);
595
+
596
+ expect(onSubscriptionUpdate).toHaveBeenCalledWith(
597
+ expect.objectContaining({
598
+ event: expect.any(Object),
599
+ subscription: expect.any(Object),
600
+ }),
601
+ );
602
+
603
+ const cancelEvent = {
604
+ type: "customer.subscription.updated",
605
+ data: {
606
+ object: {
607
+ id: "sub_123",
608
+ customer: "cus_123",
609
+ status: "active",
610
+ cancel_at_period_end: true,
611
+ items: {
612
+ data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
613
+ },
614
+ current_period_start: Math.floor(Date.now() / 1000),
615
+ current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
616
+ },
617
+ },
618
+ };
619
+
620
+ const cancelRequest = new Request(
621
+ "http://localhost:3000/api/auth/stripe/webhook",
622
+ {
623
+ method: "POST",
624
+ headers: {
625
+ "stripe-signature": "test_signature",
626
+ },
627
+ body: JSON.stringify(cancelEvent),
628
+ },
629
+ );
630
+
631
+ mockStripeForEvents.webhooks.constructEvent.mockReturnValue(cancelEvent);
632
+ await eventTestAuth.handler(cancelRequest);
633
+
634
+ expect(onSubscriptionCancel).toHaveBeenCalledWith({
635
+ event: cancelEvent,
636
+ subscription: expect.objectContaining({
637
+ id: "sub_123",
638
+ }),
639
+ stripeSubscription: expect.objectContaining({
640
+ id: "sub_123",
641
+ cancel_at_period_end: true,
642
+ }),
643
+ });
644
+
645
+ const deleteEvent = {
646
+ type: "customer.subscription.deleted",
647
+ data: {
648
+ object: {
649
+ id: "sub_123",
650
+ customer: "cus_123",
651
+ status: "canceled",
652
+ metadata: {
653
+ referenceId: "user_123",
654
+ subscriptionId: "sub_123",
655
+ },
656
+ },
657
+ },
658
+ };
659
+
660
+ const deleteRequest = new Request(
661
+ "http://localhost:3000/api/auth/stripe/webhook",
662
+ {
663
+ method: "POST",
664
+ headers: {
665
+ "stripe-signature": "test_signature",
666
+ },
667
+ body: JSON.stringify(deleteEvent),
668
+ },
669
+ );
670
+
671
+ mockStripeForEvents.webhooks.constructEvent.mockReturnValue(deleteEvent);
672
+ await eventTestAuth.handler(deleteRequest);
673
+
674
+ expect(onSubscriptionDeleted).toHaveBeenCalledWith({
675
+ event: deleteEvent,
676
+ subscription: expect.objectContaining({
677
+ id: "sub_123",
678
+ }),
679
+ stripeSubscription: expect.objectContaining({
680
+ id: "sub_123",
681
+ status: "canceled",
682
+ }),
683
+ });
684
+ });
456
685
  });
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