@better-auth/stripe 1.5.0-beta.1 → 1.5.0-beta.2

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.5.0-beta.1 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.5.0-beta.2 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,11 +7,11 @@
7
7
  ℹ entry: src/index.ts, src/client.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ dist/index.mjs 40.12 kB │ gzip: 7.63 kB
11
- ℹ dist/client.mjs  0.35 kB │ gzip: 0.25 kB
10
+ ℹ dist/index.mjs 46.22 kB │ gzip: 8.51 kB
11
+ ℹ dist/client.mjs  0.39 kB │ gzip: 0.27 kB
12
12
  ℹ dist/error-codes-qqooUh6R.mjs  0.72 kB │ gzip: 0.42 kB
13
- ℹ dist/client.d.mts  2.97 kB │ gzip: 0.79 kB
13
+ ℹ dist/client.d.mts  3.01 kB │ gzip: 0.80 kB
14
14
  ℹ dist/index.d.mts  0.21 kB │ gzip: 0.14 kB
15
- ℹ dist/index-DpiQGYLJ.d.mts 25.42 kB │ gzip: 4.88 kB
16
- ℹ 6 files, total: 69.80 kB
17
- ✔ Build complete in 16274ms
15
+ ℹ dist/index-SbT5j9k6.d.mts 26.83 kB │ gzip: 5.12 kB
16
+ ℹ 6 files, total: 77.38 kB
17
+ ✔ Build complete in 16492ms
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as stripe } from "./index-DpiQGYLJ.mjs";
1
+ import { n as stripe } from "./index-SbT5j9k6.mjs";
2
2
 
3
3
  //#region src/error-codes.d.ts
4
4
  declare const STRIPE_ERROR_CODES: {
@@ -54,6 +54,7 @@ declare const stripeClient: <O extends {
54
54
  }>>;
55
55
  pathMethods: {
56
56
  "/subscription/billing-portal": "POST";
57
+ "/subscription/restore": "POST";
57
58
  };
58
59
  $ERROR_CODES: {
59
60
  readonly SUBSCRIPTION_NOT_FOUND: {
package/dist/client.mjs CHANGED
@@ -5,7 +5,10 @@ const stripeClient = (options) => {
5
5
  return {
6
6
  id: "stripe-client",
7
7
  $InferServerPlugin: {},
8
- pathMethods: { "/subscription/billing-portal": "POST" },
8
+ pathMethods: {
9
+ "/subscription/billing-portal": "POST",
10
+ "/subscription/restore": "POST"
11
+ },
9
12
  $ERROR_CODES: STRIPE_ERROR_CODES
10
13
  };
11
14
  };
@@ -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-DpiQGYLJ.mjs";
1
+ import { a as SubscriptionOptions, i as Subscription, n as stripe, r as StripePlan, t as StripePlugin } from "./index-SbT5j9k6.mjs";
2
2
  export { StripePlan, StripePlugin, Subscription, SubscriptionOptions, stripe };
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { defineErrorCodes } from "@better-auth/core/utils";
3
3
  import { defu } from "defu";
4
4
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
5
5
  import { APIError } from "@better-auth/core/error";
6
- import { HIDE_METADATA, logger } from "better-auth";
6
+ import { HIDE_METADATA } from "better-auth";
7
7
  import { APIError as APIError$1, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
8
8
  import * as z from "zod/v4";
9
9
  import { mergeSchema } from "better-auth/db";
@@ -19,6 +19,24 @@ async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
19
19
  async function getPlanByName(options, name) {
20
20
  return await getPlans(options.subscription).then((res) => res?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()));
21
21
  }
22
+ /**
23
+ * Checks if a subscription is in an available state (active or trialing)
24
+ */
25
+ function isActiveOrTrialing(sub) {
26
+ return sub.status === "active" || sub.status === "trialing";
27
+ }
28
+ /**
29
+ * Check if a subscription is scheduled to be canceled (DB subscription object)
30
+ */
31
+ function isPendingCancel(sub) {
32
+ return !!(sub.cancelAtPeriodEnd || sub.cancelAt);
33
+ }
34
+ /**
35
+ * Check if a Stripe subscription is scheduled to be canceled (Stripe API response)
36
+ */
37
+ function isStripePendingCancel(stripeSub) {
38
+ return !!(stripeSub.cancel_at_period_end || stripeSub.cancel_at);
39
+ }
22
40
 
23
41
  //#endregion
24
42
  //#region src/hooks.ts
@@ -48,6 +66,10 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
48
66
  periodStart: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_start * 1e3),
49
67
  periodEnd: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_end * 1e3),
50
68
  stripeSubscriptionId: checkoutSession.subscription,
69
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
70
+ cancelAt: subscription.cancel_at ? /* @__PURE__ */ new Date(subscription.cancel_at * 1e3) : null,
71
+ canceledAt: subscription.canceled_at ? /* @__PURE__ */ new Date(subscription.canceled_at * 1e3) : null,
72
+ endedAt: subscription.ended_at ? /* @__PURE__ */ new Date(subscription.ended_at * 1e3) : null,
51
73
  seats,
52
74
  ...trial
53
75
  },
@@ -74,7 +96,81 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
74
96
  }
75
97
  }
76
98
  } catch (e) {
77
- logger.error(`Stripe webhook failed. Error: ${e.message}`);
99
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
100
+ }
101
+ }
102
+ async function onSubscriptionCreated(ctx, options, event) {
103
+ try {
104
+ if (!options.subscription?.enabled) return;
105
+ const subscriptionCreated = event.data.object;
106
+ const stripeCustomerId = subscriptionCreated.customer?.toString();
107
+ if (!stripeCustomerId) {
108
+ ctx.context.logger.warn(`Stripe webhook warning: customer.subscription.created event received without customer ID`);
109
+ return;
110
+ }
111
+ if (await ctx.context.adapter.findOne({
112
+ model: "subscription",
113
+ where: [{
114
+ field: "stripeSubscriptionId",
115
+ value: subscriptionCreated.id
116
+ }]
117
+ })) {
118
+ ctx.context.logger.info(`Stripe webhook: Subscription ${subscriptionCreated.id} already exists in database, skipping creation`);
119
+ return;
120
+ }
121
+ const user$1 = await ctx.context.adapter.findOne({
122
+ model: "user",
123
+ where: [{
124
+ field: "stripeCustomerId",
125
+ value: stripeCustomerId
126
+ }]
127
+ });
128
+ if (!user$1) {
129
+ ctx.context.logger.warn(`Stripe webhook warning: No user found with stripeCustomerId: ${stripeCustomerId}`);
130
+ return;
131
+ }
132
+ const subscriptionItem = subscriptionCreated.items.data[0];
133
+ if (!subscriptionItem) {
134
+ ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`);
135
+ return;
136
+ }
137
+ const priceId = subscriptionItem.price.id;
138
+ const plan = await getPlanByPriceInfo(options, priceId, subscriptionItem.price.lookup_key || null);
139
+ if (!plan) {
140
+ ctx.context.logger.warn(`Stripe webhook warning: No matching plan found for priceId: ${priceId}`);
141
+ return;
142
+ }
143
+ const seats = subscriptionItem.quantity;
144
+ const periodStart = /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3);
145
+ const periodEnd = /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3);
146
+ const trial = subscriptionCreated.trial_start && subscriptionCreated.trial_end ? {
147
+ trialStart: /* @__PURE__ */ new Date(subscriptionCreated.trial_start * 1e3),
148
+ trialEnd: /* @__PURE__ */ new Date(subscriptionCreated.trial_end * 1e3)
149
+ } : {};
150
+ const newSubscription = await ctx.context.adapter.create({
151
+ model: "subscription",
152
+ data: {
153
+ referenceId: user$1.id,
154
+ stripeCustomerId,
155
+ stripeSubscriptionId: subscriptionCreated.id,
156
+ status: subscriptionCreated.status,
157
+ plan: plan.name.toLowerCase(),
158
+ periodStart,
159
+ periodEnd,
160
+ seats,
161
+ ...plan.limits ? { limits: plan.limits } : {},
162
+ ...trial
163
+ }
164
+ });
165
+ ctx.context.logger.info(`Stripe webhook: Created subscription ${subscriptionCreated.id} for user ${user$1.id} from dashboard`);
166
+ await options.subscription?.onSubscriptionCreated?.({
167
+ event,
168
+ subscription: newSubscription,
169
+ stripeSubscription: subscriptionCreated,
170
+ plan
171
+ });
172
+ } catch (error) {
173
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
78
174
  }
79
175
  }
80
176
  async function onSubscriptionUpdated(ctx, options, event) {
@@ -104,9 +200,9 @@ async function onSubscriptionUpdated(ctx, options, event) {
104
200
  }]
105
201
  });
106
202
  if (subs.length > 1) {
107
- const activeSub = subs.find((sub) => sub.status === "active" || sub.status === "trialing");
203
+ const activeSub = subs.find((sub) => isActiveOrTrialing(sub));
108
204
  if (!activeSub) {
109
- logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
205
+ ctx.context.logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
110
206
  return;
111
207
  }
112
208
  subscription = activeSub;
@@ -125,6 +221,9 @@ async function onSubscriptionUpdated(ctx, options, event) {
125
221
  periodStart: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_start * 1e3),
126
222
  periodEnd: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_end * 1e3),
127
223
  cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
224
+ cancelAt: subscriptionUpdated.cancel_at ? /* @__PURE__ */ new Date(subscriptionUpdated.cancel_at * 1e3) : null,
225
+ canceledAt: subscriptionUpdated.canceled_at ? /* @__PURE__ */ new Date(subscriptionUpdated.canceled_at * 1e3) : null,
226
+ endedAt: subscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(subscriptionUpdated.ended_at * 1e3) : null,
128
227
  seats,
129
228
  stripeSubscriptionId: subscriptionUpdated.id
130
229
  },
@@ -133,7 +232,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
133
232
  value: subscription.id
134
233
  }]
135
234
  });
136
- if (subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end && !subscription.cancelAtPeriodEnd) await options.subscription.onSubscriptionCancel?.({
235
+ if (subscriptionUpdated.status === "active" && isStripePendingCancel(subscriptionUpdated) && !isPendingCancel(subscription)) await options.subscription.onSubscriptionCancel?.({
137
236
  subscription,
138
237
  cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
139
238
  stripeSubscription: subscriptionUpdated,
@@ -148,7 +247,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
148
247
  if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscription, ctx);
149
248
  }
150
249
  } catch (error) {
151
- logger.error(`Stripe webhook failed. Error: ${error}`);
250
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
152
251
  }
153
252
  }
154
253
  async function onSubscriptionDeleted(ctx, options, event) {
@@ -172,7 +271,11 @@ async function onSubscriptionDeleted(ctx, options, event) {
172
271
  }],
173
272
  update: {
174
273
  status: "canceled",
175
- updatedAt: /* @__PURE__ */ new Date()
274
+ updatedAt: /* @__PURE__ */ new Date(),
275
+ cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
276
+ cancelAt: subscriptionDeleted.cancel_at ? /* @__PURE__ */ new Date(subscriptionDeleted.cancel_at * 1e3) : null,
277
+ canceledAt: subscriptionDeleted.canceled_at ? /* @__PURE__ */ new Date(subscriptionDeleted.canceled_at * 1e3) : null,
278
+ endedAt: subscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(subscriptionDeleted.ended_at * 1e3) : null
176
279
  }
177
280
  });
178
281
  await options.subscription.onSubscriptionDeleted?.({
@@ -180,9 +283,9 @@ async function onSubscriptionDeleted(ctx, options, event) {
180
283
  stripeSubscription: subscriptionDeleted,
181
284
  subscription
182
285
  });
183
- } else logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
286
+ } else ctx.context.logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
184
287
  } catch (error) {
185
- logger.error(`Stripe webhook failed. Error: ${error}`);
288
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
186
289
  }
187
290
  }
188
291
 
@@ -193,7 +296,7 @@ const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddlewar
193
296
  if (!session) throw new APIError$1("UNAUTHORIZED");
194
297
  const referenceId = ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
195
298
  if (referenceId !== session.user.id && !subscriptionOptions.authorizeReference) {
196
- logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
299
+ ctx.context.logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
197
300
  throw new APIError$1("BAD_REQUEST", { message: "Reference id is not allowed. Read server logs for more details." });
198
301
  }
199
302
  /**
@@ -306,8 +409,8 @@ const upgradeSubscription = (options) => {
306
409
  value: ctx.body.referenceId || user$1.id
307
410
  }]
308
411
  });
309
- const activeOrTrialingSubscription = subscriptions$1.find((sub) => sub.status === "active" || sub.status === "trialing");
310
- const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => sub.status === "active" || sub.status === "trialing"))).find((sub) => {
412
+ const activeOrTrialingSubscription = subscriptions$1.find((sub) => isActiveOrTrialing(sub));
413
+ const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)))).find((sub) => {
311
414
  if (subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId) return sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId;
312
415
  if (activeOrTrialingSubscription?.stripeSubscriptionId) return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
313
416
  return false;
@@ -371,7 +474,7 @@ const upgradeSubscription = (options) => {
371
474
  });
372
475
  return ctx.json({
373
476
  url,
374
- redirect: true
477
+ redirect: !ctx.body.disableRedirect
375
478
  });
376
479
  }
377
480
  let subscription = activeOrTrialingSubscription || incompleteSubscription;
@@ -407,7 +510,13 @@ const upgradeSubscription = (options) => {
407
510
  plan,
408
511
  subscription
409
512
  }, ctx.request, ctx);
410
- const freeTrial = !subscriptions$1.some((s) => {
513
+ const freeTrial = !(await ctx.context.adapter.findMany({
514
+ model: "subscription",
515
+ where: [{
516
+ field: "referenceId",
517
+ value: referenceId
518
+ }]
519
+ })).some((s) => {
411
520
  return !!(s.trialStart || s.trialEnd) || s.status === "trialing";
412
521
  }) && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
413
522
  let priceIdToUse = void 0;
@@ -477,17 +586,19 @@ const cancelSubscriptionCallback = (options) => {
477
586
  value: subscriptionId
478
587
  }]
479
588
  });
480
- if (!subscription || subscription.cancelAtPeriodEnd || subscription.status === "canceled") throw ctx.redirect(getUrl(ctx, callbackURL));
589
+ if (!subscription || subscription.status === "canceled" || isPendingCancel(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
481
590
  const currentSubscription = (await client.subscriptions.list({
482
591
  customer: user$1.stripeCustomerId,
483
592
  status: "active"
484
593
  })).data.find((sub) => sub.id === subscription.stripeSubscriptionId);
485
- if (currentSubscription?.cancel_at_period_end === true) {
594
+ if (currentSubscription && isStripePendingCancel(currentSubscription) && !isPendingCancel(subscription)) {
486
595
  await ctx.context.adapter.update({
487
596
  model: "subscription",
488
597
  update: {
489
598
  status: currentSubscription?.status,
490
- cancelAtPeriodEnd: true
599
+ cancelAtPeriodEnd: currentSubscription?.cancel_at_period_end || false,
600
+ cancelAt: currentSubscription?.cancel_at ? /* @__PURE__ */ new Date(currentSubscription.cancel_at * 1e3) : null,
601
+ canceledAt: currentSubscription?.canceled_at ? /* @__PURE__ */ new Date(currentSubscription.canceled_at * 1e3) : null
491
602
  },
492
603
  where: [{
493
604
  field: "id",
@@ -510,7 +621,8 @@ const cancelSubscriptionCallback = (options) => {
510
621
  const cancelSubscriptionBodySchema = z.object({
511
622
  referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
512
623
  subscriptionId: z.string().meta({ description: "The Stripe subscription ID to cancel. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
513
- 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\"" })
624
+ 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\"" }),
625
+ disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription cancellation. Eg: true" }).default(false)
514
626
  });
515
627
  /**
516
628
  * ### Endpoint
@@ -553,10 +665,10 @@ const cancelSubscription = (options) => {
553
665
  field: "referenceId",
554
666
  value: referenceId
555
667
  }]
556
- }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
668
+ }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
557
669
  if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
558
670
  if (!subscription || !subscription.stripeCustomerId) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
559
- const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub.status === "active" || sub.status === "trialing"));
671
+ const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
560
672
  if (!activeSubscriptions.length) {
561
673
  /**
562
674
  * If the subscription is not found, we need to delete the subscription
@@ -581,29 +693,36 @@ const cancelSubscription = (options) => {
581
693
  subscription_cancel: { subscription: activeSubscription.id }
582
694
  }
583
695
  }).catch(async (e) => {
584
- if (e.message.includes("already set to be cancel")) {
696
+ if (e.message?.includes("already set to be canceled")) {
585
697
  /**
586
- * in-case we missed the event from stripe, we set it manually
698
+ * in-case we missed the event from stripe, we sync the actual state
587
699
  * this is a rare case and should not happen
588
700
  */
589
- if (!subscription.cancelAtPeriodEnd) await ctx.context.adapter.updateMany({
590
- model: "subscription",
591
- update: { cancelAtPeriodEnd: true },
592
- where: [{
593
- field: "referenceId",
594
- value: referenceId
595
- }]
596
- });
701
+ if (!isPendingCancel(subscription)) {
702
+ const stripeSub = await client.subscriptions.retrieve(activeSubscription.id);
703
+ await ctx.context.adapter.update({
704
+ model: "subscription",
705
+ update: {
706
+ cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
707
+ cancelAt: stripeSub.cancel_at ? /* @__PURE__ */ new Date(stripeSub.cancel_at * 1e3) : null,
708
+ canceledAt: stripeSub.canceled_at ? /* @__PURE__ */ new Date(stripeSub.canceled_at * 1e3) : null
709
+ },
710
+ where: [{
711
+ field: "id",
712
+ value: subscription.id
713
+ }]
714
+ });
715
+ }
597
716
  }
598
717
  throw ctx.error("BAD_REQUEST", {
599
718
  message: e.message,
600
719
  code: e.code
601
720
  });
602
721
  });
603
- return {
722
+ return ctx.json({
604
723
  url,
605
- redirect: true
606
- };
724
+ redirect: !ctx.body.disableRedirect
725
+ });
607
726
  });
608
727
  };
609
728
  const restoreSubscriptionBodySchema = z.object({
@@ -632,31 +751,36 @@ const restoreSubscription = (options) => {
632
751
  field: "referenceId",
633
752
  value: referenceId
634
753
  }]
635
- }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
754
+ }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
636
755
  if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
637
756
  if (!subscription || !subscription.stripeCustomerId) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
638
757
  if (subscription.status != "active" && subscription.status != "trialing") throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_ACTIVE);
639
- if (!subscription.cancelAtPeriodEnd) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION);
640
- const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub.status === "active" || sub.status === "trialing")[0]);
758
+ if (!isPendingCancel(subscription)) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION);
759
+ const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
641
760
  if (!activeSubscription) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
642
- try {
643
- const newSub = await client.subscriptions.update(activeSubscription.id, { cancel_at_period_end: false });
644
- await ctx.context.adapter.update({
645
- model: "subscription",
646
- update: {
647
- cancelAtPeriodEnd: false,
648
- updatedAt: /* @__PURE__ */ new Date()
649
- },
650
- where: [{
651
- field: "id",
652
- value: subscription.id
653
- }]
761
+ const updateParams = {};
762
+ if (activeSubscription.cancel_at) updateParams.cancel_at = "";
763
+ else if (activeSubscription.cancel_at_period_end) updateParams.cancel_at_period_end = false;
764
+ const newSub = await client.subscriptions.update(activeSubscription.id, updateParams).catch((e) => {
765
+ throw ctx.error("BAD_REQUEST", {
766
+ message: e.message,
767
+ code: e.code
654
768
  });
655
- return ctx.json(newSub);
656
- } catch (error) {
657
- ctx.context.logger.error("Error restoring subscription", error);
658
- throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.UNABLE_TO_CREATE_CUSTOMER);
659
- }
769
+ });
770
+ await ctx.context.adapter.update({
771
+ model: "subscription",
772
+ update: {
773
+ cancelAtPeriodEnd: false,
774
+ cancelAt: null,
775
+ canceledAt: null,
776
+ updatedAt: /* @__PURE__ */ new Date()
777
+ },
778
+ where: [{
779
+ field: "id",
780
+ value: subscription.id
781
+ }]
782
+ });
783
+ return ctx.json(newSub);
660
784
  });
661
785
  };
662
786
  const listActiveSubscriptionsQuerySchema = z.optional(z.object({ referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional() }));
@@ -700,9 +824,7 @@ const listActiveSubscriptions = (options) => {
700
824
  limits: plan?.limits,
701
825
  priceId: plan?.priceId
702
826
  };
703
- }).filter((sub) => {
704
- return sub.status === "active" || sub.status === "trialing";
705
- });
827
+ }).filter((sub) => isActiveOrTrialing(sub));
706
828
  return ctx.json(subs);
707
829
  });
708
830
  };
@@ -745,6 +867,9 @@ const subscriptionSuccess = (options) => {
745
867
  periodEnd: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_end * 1e3),
746
868
  periodStart: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_start * 1e3),
747
869
  stripeSubscriptionId: stripeSubscription.id,
870
+ cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
871
+ cancelAt: stripeSubscription.cancel_at ? /* @__PURE__ */ new Date(stripeSubscription.cancel_at * 1e3) : null,
872
+ canceledAt: stripeSubscription.canceled_at ? /* @__PURE__ */ new Date(stripeSubscription.canceled_at * 1e3) : null,
748
873
  ...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
749
874
  trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
750
875
  trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
@@ -767,7 +892,8 @@ const createBillingPortalBodySchema = z.object({
767
892
  return typeof localization === "string";
768
893
  }).optional(),
769
894
  referenceId: z.string().optional(),
770
- returnUrl: z.string().default("/")
895
+ returnUrl: z.string().default("/"),
896
+ disableRedirect: z.boolean().meta({ description: "Disable redirect after creating billing portal session. Eg: true" }).default(false)
771
897
  });
772
898
  const createBillingPortal = (options) => {
773
899
  const client = options.stripeClient;
@@ -791,7 +917,7 @@ const createBillingPortal = (options) => {
791
917
  field: "referenceId",
792
918
  value: referenceId
793
919
  }]
794
- }).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing")))?.stripeCustomerId;
920
+ }).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
795
921
  if (!customerId) throw new APIError("BAD_REQUEST", { message: "No Stripe customer found for this user" });
796
922
  try {
797
923
  const { url } = await client.billingPortal.sessions.create({
@@ -801,7 +927,7 @@ const createBillingPortal = (options) => {
801
927
  });
802
928
  return ctx.json({
803
929
  url,
804
- redirect: true
930
+ redirect: !ctx.body.disableRedirect
805
931
  });
806
932
  } catch (error) {
807
933
  ctx.context.logger.error("Error creating billing portal session", error);
@@ -840,6 +966,10 @@ const stripeWebhook = (options) => {
840
966
  await onCheckoutSessionCompleted(ctx, options, event);
841
967
  await options.onEvent?.(event);
842
968
  break;
969
+ case "customer.subscription.created":
970
+ await onSubscriptionCreated(ctx, options, event);
971
+ await options.onEvent?.(event);
972
+ break;
843
973
  case "customer.subscription.updated":
844
974
  await onSubscriptionUpdated(ctx, options, event);
845
975
  await options.onEvent?.(event);
@@ -916,6 +1046,18 @@ const subscriptions = { subscription: { fields: {
916
1046
  required: false,
917
1047
  defaultValue: false
918
1048
  },
1049
+ cancelAt: {
1050
+ type: "date",
1051
+ required: false
1052
+ },
1053
+ canceledAt: {
1054
+ type: "date",
1055
+ required: false
1056
+ },
1057
+ endedAt: {
1058
+ type: "date",
1059
+ required: false
1060
+ },
919
1061
  seats: {
920
1062
  type: "number",
921
1063
  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.5.0-beta.1",
4
+ "version": "1.5.0-beta.2",
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.5.0-beta.1",
54
- "better-auth": "1.5.0-beta.1"
53
+ "@better-auth/core": "1.5.0-beta.2",
54
+ "better-auth": "1.5.0-beta.2"
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.5.0-beta.1",
61
- "better-auth": "1.5.0-beta.1"
60
+ "@better-auth/core": "1.5.0-beta.2",
61
+ "better-auth": "1.5.0-beta.2"
62
62
  },
63
63
  "scripts": {
64
64
  "test": "vitest",
package/src/client.ts CHANGED
@@ -30,6 +30,7 @@ export const stripeClient = <
30
30
  >,
31
31
  pathMethods: {
32
32
  "/subscription/billing-portal": "POST",
33
+ "/subscription/restore": "POST",
33
34
  },
34
35
  $ERROR_CODES: STRIPE_ERROR_CODES,
35
36
  } satisfies BetterAuthClientPlugin;