@better-auth/stripe 1.4.9 → 1.4.10

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,5 +1,5 @@
1
1
 
2
- > @better-auth/stripe@1.4.9 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.4.10 build /home/runner/work/better-auth/better-auth/packages/stripe
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.17.2 powered by rolldown v1.0.0-beta.53
@@ -7,10 +7,10 @@
7
7
  ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs 40.70 kB │ gzip: 7.59 kB
10
+ ℹ dist/index.mjs 46.78 kB │ gzip: 8.45 kB
11
11
  ℹ dist/client.mjs  0.26 kB │ gzip: 0.20 kB
12
- ℹ dist/client.d.mts  0.62 kB │ gzip: 0.35 kB
12
+ ℹ dist/client.d.mts  0.62 kB │ gzip: 0.34 kB
13
13
  ℹ dist/index.d.mts  0.21 kB │ gzip: 0.14 kB
14
- ℹ dist/index-DQa3pHPd.d.mts 24.89 kB │ gzip: 4.83 kB
15
- ℹ 5 files, total: 66.68 kB
16
- ✔ Build complete in 23325ms
14
+ ℹ dist/index-DtwvPnmn.d.mts 26.30 kB │ gzip: 5.07 kB
15
+ ℹ 5 files, total: 74.17 kB
16
+ ✔ Build complete in 16656ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as stripe } from "./index-DQa3pHPd.mjs";
1
+ import { n as stripe } from "./index-DtwvPnmn.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  declare const stripeClient: <O extends {
@@ -17,8 +17,8 @@ declare const stripeClient: <O extends {
17
17
  stripeWebhookSecret: string;
18
18
  }>>;
19
19
  pathMethods: {
20
- "/subscription/restore": "POST";
21
20
  "/subscription/billing-portal": "POST";
21
+ "/subscription/restore": "POST";
22
22
  };
23
23
  };
24
24
  //#endregion
package/dist/client.mjs CHANGED
@@ -4,8 +4,8 @@ const stripeClient = (options) => {
4
4
  id: "stripe-client",
5
5
  $InferServerPlugin: {},
6
6
  pathMethods: {
7
- "/subscription/restore": "POST",
8
- "/subscription/billing-portal": "POST"
7
+ "/subscription/billing-portal": "POST",
8
+ "/subscription/restore": "POST"
9
9
  }
10
10
  };
11
11
  };
@@ -49,6 +49,18 @@ declare const subscriptions: {
49
49
  required: false;
50
50
  defaultValue: false;
51
51
  };
52
+ cancelAt: {
53
+ type: "date";
54
+ required: false;
55
+ };
56
+ canceledAt: {
57
+ type: "date";
58
+ required: false;
59
+ };
60
+ endedAt: {
61
+ type: "date";
62
+ required: false;
63
+ };
52
64
  seats: {
53
65
  type: "number";
54
66
  required: false;
@@ -198,9 +210,26 @@ interface Subscription {
198
210
  */
199
211
  periodEnd?: Date | undefined;
200
212
  /**
201
- * Cancel at period end
213
+ * Whether this subscription will (if status=active)
214
+ * or did (if status=canceled) cancel at the end of the current billing period.
202
215
  */
203
216
  cancelAtPeriodEnd?: boolean | undefined;
217
+ /**
218
+ * If the subscription is scheduled to be canceled,
219
+ * this is the time at which the cancellation will take effect.
220
+ */
221
+ cancelAt?: Date | undefined;
222
+ /**
223
+ * If the subscription has been canceled, this is the time when it was canceled.
224
+ *
225
+ * Note: If the subscription was canceled with `cancel_at_period_end`,
226
+ * this reflects the cancellation request time, not when the subscription actually ends.
227
+ */
228
+ canceledAt?: Date | undefined;
229
+ /**
230
+ * If the subscription has ended, the date the subscription ended.
231
+ */
232
+ endedAt?: Date | undefined;
204
233
  /**
205
234
  * A field to group subscriptions so you can have multiple subscriptions
206
235
  * for one reference id
@@ -279,6 +308,16 @@ type SubscriptionOptions = {
279
308
  stripeSubscription: Stripe.Subscription;
280
309
  subscription: Subscription;
281
310
  }) => Promise<void>) | undefined;
311
+ /**
312
+ * A callback to run when a subscription is created
313
+ * @returns
314
+ */
315
+ onSubscriptionCreated?: ((data: {
316
+ event: Stripe.Event;
317
+ stripeSubscription: Stripe.Subscription;
318
+ subscription: Subscription;
319
+ plan: StripePlan;
320
+ }) => Promise<void>) | undefined;
282
321
  /**
283
322
  * parameters for session create params
284
323
  *
@@ -518,6 +557,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
518
557
  referenceId: zod0.ZodOptional<zod0.ZodString>;
519
558
  subscriptionId: zod0.ZodOptional<zod0.ZodString>;
520
559
  returnUrl: zod0.ZodString;
560
+ disableRedirect: zod0.ZodDefault<zod0.ZodBoolean>;
521
561
  }, better_auth0.$strip>;
522
562
  metadata: {
523
563
  openapi: {
@@ -633,6 +673,9 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
633
673
  periodStart?: Date | undefined;
634
674
  periodEnd?: Date | undefined;
635
675
  cancelAtPeriodEnd?: boolean | undefined;
676
+ cancelAt?: Date | undefined;
677
+ canceledAt?: Date | undefined;
678
+ endedAt?: Date | undefined;
636
679
  groupId?: string | undefined;
637
680
  seats?: number | undefined;
638
681
  }[]>;
@@ -665,6 +708,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
665
708
  locale: zod0.ZodOptional<zod0.ZodCustom<Stripe.Checkout.Session.Locale, Stripe.Checkout.Session.Locale>>;
666
709
  referenceId: zod0.ZodOptional<zod0.ZodString>;
667
710
  returnUrl: zod0.ZodDefault<zod0.ZodString>;
711
+ disableRedirect: zod0.ZodDefault<zod0.ZodBoolean>;
668
712
  }, better_auth0.$strip>;
669
713
  metadata: {
670
714
  openapi: {
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SubscriptionOptions, i as Subscription, n as stripe, r as StripePlan, t as StripePlugin } from "./index-DQa3pHPd.mjs";
1
+ import { a as SubscriptionOptions, i as Subscription, n as stripe, r as StripePlan, t as StripePlugin } from "./index-DtwvPnmn.mjs";
2
2
  export { StripePlan, StripePlugin, Subscription, SubscriptionOptions, stripe };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineErrorCodes } from "@better-auth/core/utils";
2
2
  import { defu } from "defu";
3
3
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
4
- import { HIDE_METADATA, logger } from "better-auth";
4
+ import { HIDE_METADATA } from "better-auth";
5
5
  import { APIError, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
6
6
  import * as z from "zod/v4";
7
7
  import { mergeSchema } from "better-auth/db";
@@ -17,6 +17,24 @@ async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
17
17
  async function getPlanByName(options, name) {
18
18
  return await getPlans(options.subscription).then((res) => res?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()));
19
19
  }
20
+ /**
21
+ * Checks if a subscription is in an available state (active or trialing)
22
+ */
23
+ function isActiveOrTrialing(sub) {
24
+ return sub.status === "active" || sub.status === "trialing";
25
+ }
26
+ /**
27
+ * Check if a subscription is scheduled to be canceled (DB subscription object)
28
+ */
29
+ function isPendingCancel(sub) {
30
+ return !!(sub.cancelAtPeriodEnd || sub.cancelAt);
31
+ }
32
+ /**
33
+ * Check if a Stripe subscription is scheduled to be canceled (Stripe API response)
34
+ */
35
+ function isStripePendingCancel(stripeSub) {
36
+ return !!(stripeSub.cancel_at_period_end || stripeSub.cancel_at);
37
+ }
20
38
 
21
39
  //#endregion
22
40
  //#region src/hooks.ts
@@ -46,6 +64,10 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
46
64
  periodStart: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_start * 1e3),
47
65
  periodEnd: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_end * 1e3),
48
66
  stripeSubscriptionId: checkoutSession.subscription,
67
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
68
+ cancelAt: subscription.cancel_at ? /* @__PURE__ */ new Date(subscription.cancel_at * 1e3) : null,
69
+ canceledAt: subscription.canceled_at ? /* @__PURE__ */ new Date(subscription.canceled_at * 1e3) : null,
70
+ endedAt: subscription.ended_at ? /* @__PURE__ */ new Date(subscription.ended_at * 1e3) : null,
49
71
  seats,
50
72
  ...trial
51
73
  },
@@ -72,7 +94,81 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
72
94
  }
73
95
  }
74
96
  } catch (e) {
75
- logger.error(`Stripe webhook failed. Error: ${e.message}`);
97
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
98
+ }
99
+ }
100
+ async function onSubscriptionCreated(ctx, options, event) {
101
+ try {
102
+ if (!options.subscription?.enabled) return;
103
+ const subscriptionCreated = event.data.object;
104
+ const stripeCustomerId = subscriptionCreated.customer?.toString();
105
+ if (!stripeCustomerId) {
106
+ ctx.context.logger.warn(`Stripe webhook warning: customer.subscription.created event received without customer ID`);
107
+ return;
108
+ }
109
+ if (await ctx.context.adapter.findOne({
110
+ model: "subscription",
111
+ where: [{
112
+ field: "stripeSubscriptionId",
113
+ value: subscriptionCreated.id
114
+ }]
115
+ })) {
116
+ ctx.context.logger.info(`Stripe webhook: Subscription ${subscriptionCreated.id} already exists in database, skipping creation`);
117
+ return;
118
+ }
119
+ const user$1 = await ctx.context.adapter.findOne({
120
+ model: "user",
121
+ where: [{
122
+ field: "stripeCustomerId",
123
+ value: stripeCustomerId
124
+ }]
125
+ });
126
+ if (!user$1) {
127
+ ctx.context.logger.warn(`Stripe webhook warning: No user found with stripeCustomerId: ${stripeCustomerId}`);
128
+ return;
129
+ }
130
+ const subscriptionItem = subscriptionCreated.items.data[0];
131
+ if (!subscriptionItem) {
132
+ ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`);
133
+ return;
134
+ }
135
+ const priceId = subscriptionItem.price.id;
136
+ const plan = await getPlanByPriceInfo(options, priceId, subscriptionItem.price.lookup_key || null);
137
+ if (!plan) {
138
+ ctx.context.logger.warn(`Stripe webhook warning: No matching plan found for priceId: ${priceId}`);
139
+ return;
140
+ }
141
+ const seats = subscriptionItem.quantity;
142
+ const periodStart = /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3);
143
+ const periodEnd = /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3);
144
+ const trial = subscriptionCreated.trial_start && subscriptionCreated.trial_end ? {
145
+ trialStart: /* @__PURE__ */ new Date(subscriptionCreated.trial_start * 1e3),
146
+ trialEnd: /* @__PURE__ */ new Date(subscriptionCreated.trial_end * 1e3)
147
+ } : {};
148
+ const newSubscription = await ctx.context.adapter.create({
149
+ model: "subscription",
150
+ data: {
151
+ referenceId: user$1.id,
152
+ stripeCustomerId,
153
+ stripeSubscriptionId: subscriptionCreated.id,
154
+ status: subscriptionCreated.status,
155
+ plan: plan.name.toLowerCase(),
156
+ periodStart,
157
+ periodEnd,
158
+ seats,
159
+ ...plan.limits ? { limits: plan.limits } : {},
160
+ ...trial
161
+ }
162
+ });
163
+ ctx.context.logger.info(`Stripe webhook: Created subscription ${subscriptionCreated.id} for user ${user$1.id} from dashboard`);
164
+ await options.subscription?.onSubscriptionCreated?.({
165
+ event,
166
+ subscription: newSubscription,
167
+ stripeSubscription: subscriptionCreated,
168
+ plan
169
+ });
170
+ } catch (error) {
171
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
76
172
  }
77
173
  }
78
174
  async function onSubscriptionUpdated(ctx, options, event) {
@@ -102,9 +198,9 @@ async function onSubscriptionUpdated(ctx, options, event) {
102
198
  }]
103
199
  });
104
200
  if (subs.length > 1) {
105
- const activeSub = subs.find((sub) => sub.status === "active" || sub.status === "trialing");
201
+ const activeSub = subs.find((sub) => isActiveOrTrialing(sub));
106
202
  if (!activeSub) {
107
- logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
203
+ ctx.context.logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
108
204
  return;
109
205
  }
110
206
  subscription = activeSub;
@@ -123,6 +219,9 @@ async function onSubscriptionUpdated(ctx, options, event) {
123
219
  periodStart: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_start * 1e3),
124
220
  periodEnd: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_end * 1e3),
125
221
  cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
222
+ cancelAt: subscriptionUpdated.cancel_at ? /* @__PURE__ */ new Date(subscriptionUpdated.cancel_at * 1e3) : null,
223
+ canceledAt: subscriptionUpdated.canceled_at ? /* @__PURE__ */ new Date(subscriptionUpdated.canceled_at * 1e3) : null,
224
+ endedAt: subscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(subscriptionUpdated.ended_at * 1e3) : null,
126
225
  seats,
127
226
  stripeSubscriptionId: subscriptionUpdated.id
128
227
  },
@@ -131,7 +230,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
131
230
  value: subscription.id
132
231
  }]
133
232
  });
134
- if (subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end && !subscription.cancelAtPeriodEnd) await options.subscription.onSubscriptionCancel?.({
233
+ if (subscriptionUpdated.status === "active" && isStripePendingCancel(subscriptionUpdated) && !isPendingCancel(subscription)) await options.subscription.onSubscriptionCancel?.({
135
234
  subscription,
136
235
  cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
137
236
  stripeSubscription: subscriptionUpdated,
@@ -146,7 +245,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
146
245
  if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscription, ctx);
147
246
  }
148
247
  } catch (error) {
149
- logger.error(`Stripe webhook failed. Error: ${error}`);
248
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
150
249
  }
151
250
  }
152
251
  async function onSubscriptionDeleted(ctx, options, event) {
@@ -170,7 +269,11 @@ async function onSubscriptionDeleted(ctx, options, event) {
170
269
  }],
171
270
  update: {
172
271
  status: "canceled",
173
- updatedAt: /* @__PURE__ */ new Date()
272
+ updatedAt: /* @__PURE__ */ new Date(),
273
+ cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
274
+ cancelAt: subscriptionDeleted.cancel_at ? /* @__PURE__ */ new Date(subscriptionDeleted.cancel_at * 1e3) : null,
275
+ canceledAt: subscriptionDeleted.canceled_at ? /* @__PURE__ */ new Date(subscriptionDeleted.canceled_at * 1e3) : null,
276
+ endedAt: subscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(subscriptionDeleted.ended_at * 1e3) : null
174
277
  }
175
278
  });
176
279
  await options.subscription.onSubscriptionDeleted?.({
@@ -178,9 +281,9 @@ async function onSubscriptionDeleted(ctx, options, event) {
178
281
  stripeSubscription: subscriptionDeleted,
179
282
  subscription
180
283
  });
181
- } else logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
284
+ } else ctx.context.logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
182
285
  } catch (error) {
183
- logger.error(`Stripe webhook failed. Error: ${error}`);
286
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
184
287
  }
185
288
  }
186
289
 
@@ -191,7 +294,7 @@ const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddlewar
191
294
  if (!session) throw new APIError("UNAUTHORIZED");
192
295
  const referenceId = ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
193
296
  if (referenceId !== session.user.id && !subscriptionOptions.authorizeReference) {
194
- logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
297
+ ctx.context.logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
195
298
  throw new APIError("BAD_REQUEST", { message: "Reference id is not allowed. Read server logs for more details." });
196
299
  }
197
300
  /**
@@ -314,8 +417,8 @@ const upgradeSubscription = (options) => {
314
417
  value: ctx.body.referenceId || user$1.id
315
418
  }]
316
419
  });
317
- const activeOrTrialingSubscription = subscriptions$1.find((sub) => sub.status === "active" || sub.status === "trialing");
318
- const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => sub.status === "active" || sub.status === "trialing"))).find((sub) => {
420
+ const activeOrTrialingSubscription = subscriptions$1.find((sub) => isActiveOrTrialing(sub));
421
+ const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)))).find((sub) => {
319
422
  if (subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId) return sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId;
320
423
  if (activeOrTrialingSubscription?.stripeSubscriptionId) return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
321
424
  return false;
@@ -379,7 +482,7 @@ const upgradeSubscription = (options) => {
379
482
  });
380
483
  return ctx.json({
381
484
  url,
382
- redirect: true
485
+ redirect: !ctx.body.disableRedirect
383
486
  });
384
487
  }
385
488
  let subscription = activeOrTrialingSubscription || incompleteSubscription;
@@ -415,7 +518,13 @@ const upgradeSubscription = (options) => {
415
518
  plan,
416
519
  subscription
417
520
  }, ctx.request, ctx);
418
- const freeTrial = !subscriptions$1.some((s) => {
521
+ const freeTrial = !(await ctx.context.adapter.findMany({
522
+ model: "subscription",
523
+ where: [{
524
+ field: "referenceId",
525
+ value: referenceId
526
+ }]
527
+ })).some((s) => {
419
528
  return !!(s.trialStart || s.trialEnd) || s.status === "trialing";
420
529
  }) && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
421
530
  let priceIdToUse = void 0;
@@ -485,17 +594,19 @@ const cancelSubscriptionCallback = (options) => {
485
594
  value: subscriptionId
486
595
  }]
487
596
  });
488
- if (!subscription || subscription.cancelAtPeriodEnd || subscription.status === "canceled") throw ctx.redirect(getUrl(ctx, callbackURL));
597
+ if (!subscription || subscription.status === "canceled" || isPendingCancel(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
489
598
  const currentSubscription = (await client.subscriptions.list({
490
599
  customer: user$1.stripeCustomerId,
491
600
  status: "active"
492
601
  })).data.find((sub) => sub.id === subscription.stripeSubscriptionId);
493
- if (currentSubscription?.cancel_at_period_end === true) {
602
+ if (currentSubscription && isStripePendingCancel(currentSubscription) && !isPendingCancel(subscription)) {
494
603
  await ctx.context.adapter.update({
495
604
  model: "subscription",
496
605
  update: {
497
606
  status: currentSubscription?.status,
498
- cancelAtPeriodEnd: true
607
+ cancelAtPeriodEnd: currentSubscription?.cancel_at_period_end || false,
608
+ cancelAt: currentSubscription?.cancel_at ? /* @__PURE__ */ new Date(currentSubscription.cancel_at * 1e3) : null,
609
+ canceledAt: currentSubscription?.canceled_at ? /* @__PURE__ */ new Date(currentSubscription.canceled_at * 1e3) : null
499
610
  },
500
611
  where: [{
501
612
  field: "id",
@@ -518,7 +629,8 @@ const cancelSubscriptionCallback = (options) => {
518
629
  const cancelSubscriptionBodySchema = z.object({
519
630
  referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
520
631
  subscriptionId: z.string().meta({ description: "The Stripe subscription ID to cancel. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
521
- returnUrl: z.string().meta({ description: "URL to take customers to when they click on the billing portal's link to return to your website. Eg: \"/account\"" })
632
+ returnUrl: z.string().meta({ description: "URL to take customers to when they click on the billing portal's link to return to your website. Eg: \"/account\"" }),
633
+ disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription cancellation. Eg: true" }).default(false)
522
634
  });
523
635
  /**
524
636
  * ### Endpoint
@@ -561,10 +673,10 @@ const cancelSubscription = (options) => {
561
673
  field: "referenceId",
562
674
  value: referenceId
563
675
  }]
564
- }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
676
+ }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
565
677
  if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
566
678
  if (!subscription || !subscription.stripeCustomerId) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
567
- const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub.status === "active" || sub.status === "trialing"));
679
+ const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
568
680
  if (!activeSubscriptions.length) {
569
681
  /**
570
682
  * If the subscription is not found, we need to delete the subscription
@@ -589,29 +701,36 @@ const cancelSubscription = (options) => {
589
701
  subscription_cancel: { subscription: activeSubscription.id }
590
702
  }
591
703
  }).catch(async (e) => {
592
- if (e.message.includes("already set to be cancel")) {
704
+ if (e.message?.includes("already set to be canceled")) {
593
705
  /**
594
- * in-case we missed the event from stripe, we set it manually
706
+ * in-case we missed the event from stripe, we sync the actual state
595
707
  * this is a rare case and should not happen
596
708
  */
597
- if (!subscription.cancelAtPeriodEnd) await ctx.context.adapter.updateMany({
598
- model: "subscription",
599
- update: { cancelAtPeriodEnd: true },
600
- where: [{
601
- field: "referenceId",
602
- value: referenceId
603
- }]
604
- });
709
+ if (!isPendingCancel(subscription)) {
710
+ const stripeSub = await client.subscriptions.retrieve(activeSubscription.id);
711
+ await ctx.context.adapter.update({
712
+ model: "subscription",
713
+ update: {
714
+ cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
715
+ cancelAt: stripeSub.cancel_at ? /* @__PURE__ */ new Date(stripeSub.cancel_at * 1e3) : null,
716
+ canceledAt: stripeSub.canceled_at ? /* @__PURE__ */ new Date(stripeSub.canceled_at * 1e3) : null
717
+ },
718
+ where: [{
719
+ field: "id",
720
+ value: subscription.id
721
+ }]
722
+ });
723
+ }
605
724
  }
606
725
  throw ctx.error("BAD_REQUEST", {
607
726
  message: e.message,
608
727
  code: e.code
609
728
  });
610
729
  });
611
- return {
730
+ return ctx.json({
612
731
  url,
613
- redirect: true
614
- };
732
+ redirect: !ctx.body.disableRedirect
733
+ });
615
734
  });
616
735
  };
617
736
  const restoreSubscriptionBodySchema = z.object({
@@ -640,31 +759,36 @@ const restoreSubscription = (options) => {
640
759
  field: "referenceId",
641
760
  value: referenceId
642
761
  }]
643
- }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
762
+ }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
644
763
  if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
645
764
  if (!subscription || !subscription.stripeCustomerId) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
646
765
  if (subscription.status != "active" && subscription.status != "trialing") throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_ACTIVE });
647
- if (!subscription.cancelAtPeriodEnd) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION });
648
- const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub.status === "active" || sub.status === "trialing")[0]);
766
+ if (!isPendingCancel(subscription)) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION });
767
+ const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
649
768
  if (!activeSubscription) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
650
- try {
651
- const newSub = await client.subscriptions.update(activeSubscription.id, { cancel_at_period_end: false });
652
- await ctx.context.adapter.update({
653
- model: "subscription",
654
- update: {
655
- cancelAtPeriodEnd: false,
656
- updatedAt: /* @__PURE__ */ new Date()
657
- },
658
- where: [{
659
- field: "id",
660
- value: subscription.id
661
- }]
769
+ const updateParams = {};
770
+ if (activeSubscription.cancel_at) updateParams.cancel_at = "";
771
+ else if (activeSubscription.cancel_at_period_end) updateParams.cancel_at_period_end = false;
772
+ const newSub = await client.subscriptions.update(activeSubscription.id, updateParams).catch((e) => {
773
+ throw ctx.error("BAD_REQUEST", {
774
+ message: e.message,
775
+ code: e.code
662
776
  });
663
- return ctx.json(newSub);
664
- } catch (error) {
665
- ctx.context.logger.error("Error restoring subscription", error);
666
- throw new APIError("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.UNABLE_TO_CREATE_CUSTOMER });
667
- }
777
+ });
778
+ await ctx.context.adapter.update({
779
+ model: "subscription",
780
+ update: {
781
+ cancelAtPeriodEnd: false,
782
+ cancelAt: null,
783
+ canceledAt: null,
784
+ updatedAt: /* @__PURE__ */ new Date()
785
+ },
786
+ where: [{
787
+ field: "id",
788
+ value: subscription.id
789
+ }]
790
+ });
791
+ return ctx.json(newSub);
668
792
  });
669
793
  };
670
794
  const listActiveSubscriptionsQuerySchema = z.optional(z.object({ referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional() }));
@@ -708,9 +832,7 @@ const listActiveSubscriptions = (options) => {
708
832
  limits: plan?.limits,
709
833
  priceId: plan?.priceId
710
834
  };
711
- }).filter((sub) => {
712
- return sub.status === "active" || sub.status === "trialing";
713
- });
835
+ }).filter((sub) => isActiveOrTrialing(sub));
714
836
  return ctx.json(subs);
715
837
  });
716
838
  };
@@ -753,6 +875,9 @@ const subscriptionSuccess = (options) => {
753
875
  periodEnd: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_end * 1e3),
754
876
  periodStart: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_start * 1e3),
755
877
  stripeSubscriptionId: stripeSubscription.id,
878
+ cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
879
+ cancelAt: stripeSubscription.cancel_at ? /* @__PURE__ */ new Date(stripeSubscription.cancel_at * 1e3) : null,
880
+ canceledAt: stripeSubscription.canceled_at ? /* @__PURE__ */ new Date(stripeSubscription.canceled_at * 1e3) : null,
756
881
  ...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
757
882
  trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
758
883
  trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
@@ -775,7 +900,8 @@ const createBillingPortalBodySchema = z.object({
775
900
  return typeof localization === "string";
776
901
  }).optional(),
777
902
  referenceId: z.string().optional(),
778
- returnUrl: z.string().default("/")
903
+ returnUrl: z.string().default("/"),
904
+ disableRedirect: z.boolean().meta({ description: "Disable redirect after creating billing portal session. Eg: true" }).default(false)
779
905
  });
780
906
  const createBillingPortal = (options) => {
781
907
  const client = options.stripeClient;
@@ -799,7 +925,7 @@ const createBillingPortal = (options) => {
799
925
  field: "referenceId",
800
926
  value: referenceId
801
927
  }]
802
- }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing")))?.stripeCustomerId;
928
+ }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
803
929
  if (!customerId) throw new APIError("BAD_REQUEST", { message: "No Stripe customer found for this user" });
804
930
  try {
805
931
  const { url } = await client.billingPortal.sessions.create({
@@ -809,7 +935,7 @@ const createBillingPortal = (options) => {
809
935
  });
810
936
  return ctx.json({
811
937
  url,
812
- redirect: true
938
+ redirect: !ctx.body.disableRedirect
813
939
  });
814
940
  } catch (error) {
815
941
  ctx.context.logger.error("Error creating billing portal session", error);
@@ -848,6 +974,10 @@ const stripeWebhook = (options) => {
848
974
  await onCheckoutSessionCompleted(ctx, options, event);
849
975
  await options.onEvent?.(event);
850
976
  break;
977
+ case "customer.subscription.created":
978
+ await onSubscriptionCreated(ctx, options, event);
979
+ await options.onEvent?.(event);
980
+ break;
851
981
  case "customer.subscription.updated":
852
982
  await onSubscriptionUpdated(ctx, options, event);
853
983
  await options.onEvent?.(event);
@@ -924,6 +1054,18 @@ const subscriptions = { subscription: { fields: {
924
1054
  required: false,
925
1055
  defaultValue: false
926
1056
  },
1057
+ cancelAt: {
1058
+ type: "date",
1059
+ required: false
1060
+ },
1061
+ canceledAt: {
1062
+ type: "date",
1063
+ required: false
1064
+ },
1065
+ endedAt: {
1066
+ type: "date",
1067
+ required: false
1068
+ },
927
1069
  seats: {
928
1070
  type: "number",
929
1071
  required: false
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/stripe",
3
3
  "author": "Bereket Engida",
4
- "version": "1.4.9",
4
+ "version": "1.4.10",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
7
7
  "types": "dist/index.d.mts",
@@ -50,15 +50,15 @@
50
50
  },
51
51
  "peerDependencies": {
52
52
  "stripe": "^18 || ^19 || ^20",
53
- "@better-auth/core": "1.4.9",
54
- "better-auth": "1.4.9"
53
+ "better-auth": "1.4.10",
54
+ "@better-auth/core": "1.4.10"
55
55
  },
56
56
  "devDependencies": {
57
57
  "better-call": "1.1.7",
58
58
  "stripe": "^20.0.0",
59
59
  "tsdown": "^0.17.2",
60
- "@better-auth/core": "1.4.9",
61
- "better-auth": "1.4.9"
60
+ "@better-auth/core": "1.4.10",
61
+ "better-auth": "1.4.10"
62
62
  },
63
63
  "scripts": {
64
64
  "test": "vitest",
package/src/client.ts CHANGED
@@ -28,8 +28,8 @@ export const stripeClient = <
28
28
  >
29
29
  >,
30
30
  pathMethods: {
31
- "/subscription/restore": "POST",
32
31
  "/subscription/billing-portal": "POST",
32
+ "/subscription/restore": "POST",
33
33
  },
34
34
  } satisfies BetterAuthClientPlugin;
35
35
  };