@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.
package/src/hooks.ts CHANGED
@@ -1,8 +1,13 @@
1
- import type { GenericEndpointContext } from "better-auth";
2
- import { logger } from "better-auth";
1
+ import type { GenericEndpointContext } from "@better-auth/core";
2
+ import type { User } from "@better-auth/core/db";
3
3
  import type Stripe from "stripe";
4
4
  import type { InputSubscription, StripeOptions, Subscription } from "./types";
5
- import { getPlanByPriceInfo } from "./utils";
5
+ import {
6
+ getPlanByPriceInfo,
7
+ isActiveOrTrialing,
8
+ isPendingCancel,
9
+ isStripePendingCancel,
10
+ } from "./utils";
6
11
 
7
12
  export async function onCheckoutSessionCompleted(
8
13
  ctx: GenericEndpointContext,
@@ -54,7 +59,17 @@ export async function onCheckoutSessionCompleted(
54
59
  subscription.items.data[0]!.current_period_end * 1000,
55
60
  ),
56
61
  stripeSubscriptionId: checkoutSession.subscription as string,
57
- seats,
62
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
63
+ cancelAt: subscription.cancel_at
64
+ ? new Date(subscription.cancel_at * 1000)
65
+ : null,
66
+ canceledAt: subscription.canceled_at
67
+ ? new Date(subscription.canceled_at * 1000)
68
+ : null,
69
+ endedAt: subscription.ended_at
70
+ ? new Date(subscription.ended_at * 1000)
71
+ : null,
72
+ seats: seats,
58
73
  ...trial,
59
74
  },
60
75
  where: [
@@ -93,7 +108,123 @@ export async function onCheckoutSessionCompleted(
93
108
  }
94
109
  }
95
110
  } catch (e: any) {
96
- logger.error(`Stripe webhook failed. Error: ${e.message}`);
111
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
112
+ }
113
+ }
114
+
115
+ export async function onSubscriptionCreated(
116
+ ctx: GenericEndpointContext,
117
+ options: StripeOptions,
118
+ event: Stripe.Event,
119
+ ) {
120
+ try {
121
+ if (!options.subscription?.enabled) {
122
+ return;
123
+ }
124
+
125
+ const subscriptionCreated = event.data.object as Stripe.Subscription;
126
+ const stripeCustomerId = subscriptionCreated.customer?.toString();
127
+ if (!stripeCustomerId) {
128
+ ctx.context.logger.warn(
129
+ `Stripe webhook warning: customer.subscription.created event received without customer ID`,
130
+ );
131
+ return;
132
+ }
133
+
134
+ // Check if subscription already exists in database
135
+ const existingSubscription =
136
+ await ctx.context.adapter.findOne<Subscription>({
137
+ model: "subscription",
138
+ where: [
139
+ {
140
+ field: "stripeSubscriptionId",
141
+ value: subscriptionCreated.id,
142
+ },
143
+ ],
144
+ });
145
+ if (existingSubscription) {
146
+ ctx.context.logger.info(
147
+ `Stripe webhook: Subscription ${subscriptionCreated.id} already exists in database, skipping creation`,
148
+ );
149
+ return;
150
+ }
151
+
152
+ // Find user by stripeCustomerId
153
+ const user = await ctx.context.adapter.findOne<User>({
154
+ model: "user",
155
+ where: [
156
+ {
157
+ field: "stripeCustomerId",
158
+ value: stripeCustomerId,
159
+ },
160
+ ],
161
+ });
162
+ if (!user) {
163
+ ctx.context.logger.warn(
164
+ `Stripe webhook warning: No user found with stripeCustomerId: ${stripeCustomerId}`,
165
+ );
166
+ return;
167
+ }
168
+
169
+ const subscriptionItem = subscriptionCreated.items.data[0];
170
+ if (!subscriptionItem) {
171
+ ctx.context.logger.warn(
172
+ `Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`,
173
+ );
174
+ return;
175
+ }
176
+
177
+ const priceId = subscriptionItem.price.id;
178
+ const priceLookupKey = subscriptionItem.price.lookup_key || null;
179
+ const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
180
+ if (!plan) {
181
+ ctx.context.logger.warn(
182
+ `Stripe webhook warning: No matching plan found for priceId: ${priceId}`,
183
+ );
184
+ return;
185
+ }
186
+
187
+ const seats = subscriptionItem.quantity;
188
+ const periodStart = new Date(subscriptionItem.current_period_start * 1000);
189
+ const periodEnd = new Date(subscriptionItem.current_period_end * 1000);
190
+
191
+ const trial =
192
+ subscriptionCreated.trial_start && subscriptionCreated.trial_end
193
+ ? {
194
+ trialStart: new Date(subscriptionCreated.trial_start * 1000),
195
+ trialEnd: new Date(subscriptionCreated.trial_end * 1000),
196
+ }
197
+ : {};
198
+
199
+ // Create the subscription in the database
200
+ const newSubscription = await ctx.context.adapter.create<Subscription>({
201
+ model: "subscription",
202
+ data: {
203
+ referenceId: user.id,
204
+ stripeCustomerId: stripeCustomerId,
205
+ stripeSubscriptionId: subscriptionCreated.id,
206
+ status: subscriptionCreated.status,
207
+ plan: plan.name.toLowerCase(),
208
+ periodStart,
209
+ periodEnd,
210
+ seats,
211
+ ...(plan.limits ? { limits: plan.limits } : {}),
212
+ ...trial,
213
+ },
214
+ });
215
+
216
+ ctx.context.logger.info(
217
+ `Stripe webhook: Created subscription ${subscriptionCreated.id} for user ${user.id} from dashboard`,
218
+ );
219
+
220
+ await options.subscription?.onSubscriptionCreated?.({
221
+ event,
222
+ subscription: newSubscription,
223
+ stripeSubscription: subscriptionCreated,
224
+ plan,
225
+ });
226
+ } catch (error: any) {
227
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
97
228
  }
98
229
  }
99
230
 
@@ -126,12 +257,11 @@ export async function onSubscriptionUpdated(
126
257
  where: [{ field: "stripeCustomerId", value: customerId }],
127
258
  });
128
259
  if (subs.length > 1) {
129
- const activeSub = subs.find(
130
- (sub: Subscription) =>
131
- sub.status === "active" || sub.status === "trialing",
260
+ const activeSub = subs.find((sub: Subscription) =>
261
+ isActiveOrTrialing(sub),
132
262
  );
133
263
  if (!activeSub) {
134
- logger.warn(
264
+ ctx.context.logger.warn(
135
265
  `Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`,
136
266
  );
137
267
  return;
@@ -161,7 +291,16 @@ export async function onSubscriptionUpdated(
161
291
  subscriptionUpdated.items.data[0]!.current_period_end * 1000,
162
292
  ),
163
293
  cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
164
- seats,
294
+ cancelAt: subscriptionUpdated.cancel_at
295
+ ? new Date(subscriptionUpdated.cancel_at * 1000)
296
+ : null,
297
+ canceledAt: subscriptionUpdated.canceled_at
298
+ ? new Date(subscriptionUpdated.canceled_at * 1000)
299
+ : null,
300
+ endedAt: subscriptionUpdated.ended_at
301
+ ? new Date(subscriptionUpdated.ended_at * 1000)
302
+ : null,
303
+ seats: seats,
165
304
  stripeSubscriptionId: subscriptionUpdated.id,
166
305
  },
167
306
  where: [
@@ -171,11 +310,11 @@ export async function onSubscriptionUpdated(
171
310
  },
172
311
  ],
173
312
  });
174
- const subscriptionCanceled =
313
+ const isNewCancellation =
175
314
  subscriptionUpdated.status === "active" &&
176
- subscriptionUpdated.cancel_at_period_end &&
177
- !subscription.cancelAtPeriodEnd; //if this is true, it means the subscription was canceled before the event was triggered
178
- if (subscriptionCanceled) {
315
+ isStripePendingCancel(subscriptionUpdated) &&
316
+ !isPendingCancel(subscription);
317
+ if (isNewCancellation) {
179
318
  await options.subscription.onSubscriptionCancel?.({
180
319
  subscription,
181
320
  cancellationDetails:
@@ -205,7 +344,7 @@ export async function onSubscriptionUpdated(
205
344
  }
206
345
  }
207
346
  } catch (error: any) {
208
- logger.error(`Stripe webhook failed. Error: ${error}`);
347
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
209
348
  }
210
349
  }
211
350
 
@@ -241,6 +380,16 @@ export async function onSubscriptionDeleted(
241
380
  update: {
242
381
  status: "canceled",
243
382
  updatedAt: new Date(),
383
+ cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
384
+ cancelAt: subscriptionDeleted.cancel_at
385
+ ? new Date(subscriptionDeleted.cancel_at * 1000)
386
+ : null,
387
+ canceledAt: subscriptionDeleted.canceled_at
388
+ ? new Date(subscriptionDeleted.canceled_at * 1000)
389
+ : null,
390
+ endedAt: subscriptionDeleted.ended_at
391
+ ? new Date(subscriptionDeleted.ended_at * 1000)
392
+ : null,
244
393
  },
245
394
  });
246
395
  await options.subscription.onSubscriptionDeleted?.({
@@ -249,11 +398,11 @@ export async function onSubscriptionDeleted(
249
398
  subscription,
250
399
  });
251
400
  } else {
252
- logger.warn(
401
+ ctx.context.logger.warn(
253
402
  `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
254
403
  );
255
404
  }
256
405
  } catch (error: any) {
257
- logger.error(`Stripe webhook failed. Error: ${error}`);
406
+ ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
258
407
  }
259
408
  }
package/src/middleware.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { createAuthMiddleware } from "@better-auth/core/api";
2
- import { logger } from "better-auth";
3
2
  import { APIError } from "better-auth/api";
4
3
  import type { SubscriptionOptions } from "./types";
5
4
 
@@ -24,7 +23,7 @@ export const referenceMiddleware = (
24
23
  referenceId !== session.user.id &&
25
24
  !subscriptionOptions.authorizeReference
26
25
  ) {
27
- logger.error(
26
+ ctx.context.logger.error(
28
27
  `Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`,
29
28
  );
30
29
  throw new APIError("BAD_REQUEST", {
package/src/routes.ts CHANGED
@@ -13,6 +13,7 @@ import type { Stripe as StripeType } from "stripe";
13
13
  import * as z from "zod/v4";
14
14
  import {
15
15
  onCheckoutSessionCompleted,
16
+ onSubscriptionCreated,
16
17
  onSubscriptionDeleted,
17
18
  onSubscriptionUpdated,
18
19
  } from "./hooks";
@@ -23,7 +24,14 @@ import type {
23
24
  Subscription,
24
25
  SubscriptionOptions,
25
26
  } from "./types";
26
- import { getPlanByName, getPlanByPriceInfo, getPlans } from "./utils";
27
+ import {
28
+ getPlanByName,
29
+ getPlanByPriceInfo,
30
+ getPlans,
31
+ isActiveOrTrialing,
32
+ isPendingCancel,
33
+ isStripePendingCancel,
34
+ } from "./utils";
27
35
 
28
36
  const STRIPE_ERROR_CODES = defineErrorCodes({
29
37
  SUBSCRIPTION_NOT_FOUND: "Subscription not found",
@@ -272,19 +280,15 @@ export const upgradeSubscription = (options: StripeOptions) => {
272
280
  ],
273
281
  });
274
282
 
275
- const activeOrTrialingSubscription = subscriptions.find(
276
- (sub) => sub.status === "active" || sub.status === "trialing",
283
+ const activeOrTrialingSubscription = subscriptions.find((sub) =>
284
+ isActiveOrTrialing(sub),
277
285
  );
278
286
 
279
287
  const activeSubscriptions = await client.subscriptions
280
288
  .list({
281
289
  customer: customerId,
282
290
  })
283
- .then((res) =>
284
- res.data.filter(
285
- (sub) => sub.status === "active" || sub.status === "trialing",
286
- ),
287
- );
291
+ .then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
288
292
 
289
293
  const activeSubscription = activeSubscriptions.find((sub) => {
290
294
  // If we have a specific subscription to update, match by ID
@@ -408,7 +412,7 @@ export const upgradeSubscription = (options: StripeOptions) => {
408
412
  });
409
413
  return ctx.json({
410
414
  url,
411
- redirect: true,
415
+ redirect: !ctx.body.disableRedirect,
412
416
  });
413
417
  }
414
418
 
@@ -465,7 +469,13 @@ export const upgradeSubscription = (options: StripeOptions) => {
465
469
  ctx,
466
470
  );
467
471
 
468
- const hasEverTrialed = subscriptions.some((s) => {
472
+ const allSubscriptions = await ctx.context.adapter.findMany<Subscription>(
473
+ {
474
+ model: "subscription",
475
+ where: [{ field: "referenceId", value: referenceId }],
476
+ },
477
+ );
478
+ const hasEverTrialed = allSubscriptions.some((s) => {
469
479
  // Check if user has ever had a trial for any plan (not just the same plan)
470
480
  // This prevents users from getting multiple trials by switching plans
471
481
  const hadTrial =
@@ -599,8 +609,8 @@ export const cancelSubscriptionCallback = (options: StripeOptions) => {
599
609
  });
600
610
  if (
601
611
  !subscription ||
602
- subscription.cancelAtPeriodEnd ||
603
- subscription.status === "canceled"
612
+ subscription.status === "canceled" ||
613
+ isPendingCancel(subscription)
604
614
  ) {
605
615
  throw ctx.redirect(getUrl(ctx, callbackURL));
606
616
  }
@@ -612,12 +622,24 @@ export const cancelSubscriptionCallback = (options: StripeOptions) => {
612
622
  const currentSubscription = stripeSubscription.data.find(
613
623
  (sub) => sub.id === subscription.stripeSubscriptionId,
614
624
  );
615
- if (currentSubscription?.cancel_at_period_end === true) {
625
+
626
+ const isNewCancellation =
627
+ currentSubscription &&
628
+ isStripePendingCancel(currentSubscription) &&
629
+ !isPendingCancel(subscription);
630
+ if (isNewCancellation) {
616
631
  await ctx.context.adapter.update({
617
632
  model: "subscription",
618
633
  update: {
619
634
  status: currentSubscription?.status,
620
- cancelAtPeriodEnd: true,
635
+ cancelAtPeriodEnd:
636
+ currentSubscription?.cancel_at_period_end || false,
637
+ cancelAt: currentSubscription?.cancel_at
638
+ ? new Date(currentSubscription.cancel_at * 1000)
639
+ : null,
640
+ canceledAt: currentSubscription?.canceled_at
641
+ ? new Date(currentSubscription.canceled_at * 1000)
642
+ : null,
621
643
  },
622
644
  where: [
623
645
  {
@@ -663,6 +685,16 @@ const cancelSubscriptionBodySchema = z.object({
663
685
  description:
664
686
  'URL to take customers to when they click on the billing portal\'s link to return to your website. Eg: "/account"',
665
687
  }),
688
+ /**
689
+ * Disable Redirect
690
+ */
691
+ disableRedirect: z
692
+ .boolean()
693
+ .meta({
694
+ description:
695
+ "Disable redirect after successful subscription cancellation. Eg: true",
696
+ })
697
+ .default(false),
666
698
  });
667
699
 
668
700
  /**
@@ -716,13 +748,7 @@ export const cancelSubscription = (options: StripeOptions) => {
716
748
  model: "subscription",
717
749
  where: [{ field: "referenceId", value: referenceId }],
718
750
  })
719
- .then((subs) =>
720
- subs.find(
721
- (sub) => sub.status === "active" || sub.status === "trialing",
722
- ),
723
- );
724
-
725
- // Ensure the specified subscription belongs to the (validated) referenceId.
751
+ .then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
726
752
  if (
727
753
  ctx.body.subscriptionId &&
728
754
  subscription &&
@@ -740,11 +766,7 @@ export const cancelSubscription = (options: StripeOptions) => {
740
766
  .list({
741
767
  customer: subscription.stripeCustomerId,
742
768
  })
743
- .then((res) =>
744
- res.data.filter(
745
- (sub) => sub.status === "active" || sub.status === "trialing",
746
- ),
747
- );
769
+ .then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
748
770
  if (!activeSubscriptions.length) {
749
771
  /**
750
772
  * If the subscription is not found, we need to delete the subscription
@@ -790,21 +812,30 @@ export const cancelSubscription = (options: StripeOptions) => {
790
812
  },
791
813
  })
792
814
  .catch(async (e) => {
793
- if (e.message.includes("already set to be cancel")) {
815
+ if (e.message?.includes("already set to be canceled")) {
794
816
  /**
795
- * in-case we missed the event from stripe, we set it manually
817
+ * in-case we missed the event from stripe, we sync the actual state
796
818
  * this is a rare case and should not happen
797
819
  */
798
- if (!subscription.cancelAtPeriodEnd) {
799
- await ctx.context.adapter.updateMany({
820
+ if (!isPendingCancel(subscription)) {
821
+ const stripeSub = await client.subscriptions.retrieve(
822
+ activeSubscription.id,
823
+ );
824
+ await ctx.context.adapter.update({
800
825
  model: "subscription",
801
826
  update: {
802
- cancelAtPeriodEnd: true,
827
+ cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
828
+ cancelAt: stripeSub.cancel_at
829
+ ? new Date(stripeSub.cancel_at * 1000)
830
+ : null,
831
+ canceledAt: stripeSub.canceled_at
832
+ ? new Date(stripeSub.canceled_at * 1000)
833
+ : null,
803
834
  },
804
835
  where: [
805
836
  {
806
- field: "referenceId",
807
- value: referenceId,
837
+ field: "id",
838
+ value: subscription.id,
808
839
  },
809
840
  ],
810
841
  });
@@ -815,10 +846,10 @@ export const cancelSubscription = (options: StripeOptions) => {
815
846
  code: e.code,
816
847
  });
817
848
  });
818
- return {
849
+ return ctx.json({
819
850
  url,
820
- redirect: true,
821
- };
851
+ redirect: !ctx.body.disableRedirect,
852
+ });
822
853
  },
823
854
  );
824
855
  };
@@ -880,11 +911,7 @@ export const restoreSubscription = (options: StripeOptions) => {
880
911
  },
881
912
  ],
882
913
  })
883
- .then((subs) =>
884
- subs.find(
885
- (sub) => sub.status === "active" || sub.status === "trialing",
886
- ),
887
- );
914
+ .then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
888
915
  if (
889
916
  ctx.body.subscriptionId &&
890
917
  subscription &&
@@ -905,7 +932,7 @@ export const restoreSubscription = (options: StripeOptions) => {
905
932
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
906
933
  });
907
934
  }
908
- if (!subscription.cancelAtPeriodEnd) {
935
+ if (!isPendingCancel(subscription)) {
909
936
  throw ctx.error("BAD_REQUEST", {
910
937
  message:
911
938
  STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION,
@@ -916,47 +943,48 @@ export const restoreSubscription = (options: StripeOptions) => {
916
943
  .list({
917
944
  customer: subscription.stripeCustomerId,
918
945
  })
919
- .then(
920
- (res) =>
921
- res.data.filter(
922
- (sub) => sub.status === "active" || sub.status === "trialing",
923
- )[0],
924
- );
946
+ .then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
925
947
  if (!activeSubscription) {
926
948
  throw ctx.error("BAD_REQUEST", {
927
949
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
928
950
  });
929
951
  }
930
952
 
931
- try {
932
- const newSub = await client.subscriptions.update(
933
- activeSubscription.id,
934
- {
935
- cancel_at_period_end: false,
936
- },
937
- );
953
+ // Clear scheduled cancellation based on Stripe subscription state
954
+ // Note: Stripe doesn't accept both `cancel_at` and `cancel_at_period_end` simultaneously
955
+ const updateParams: Stripe.SubscriptionUpdateParams = {};
956
+ if (activeSubscription.cancel_at) {
957
+ updateParams.cancel_at = "";
958
+ } else if (activeSubscription.cancel_at_period_end) {
959
+ updateParams.cancel_at_period_end = false;
960
+ }
938
961
 
939
- await ctx.context.adapter.update({
940
- model: "subscription",
941
- update: {
942
- cancelAtPeriodEnd: false,
943
- updatedAt: new Date(),
944
- },
945
- where: [
946
- {
947
- field: "id",
948
- value: subscription.id,
949
- },
950
- ],
962
+ const newSub = await client.subscriptions
963
+ .update(activeSubscription.id, updateParams)
964
+ .catch((e) => {
965
+ throw ctx.error("BAD_REQUEST", {
966
+ message: e.message,
967
+ code: e.code,
968
+ });
951
969
  });
952
970
 
953
- return ctx.json(newSub);
954
- } catch (error) {
955
- ctx.context.logger.error("Error restoring subscription", error);
956
- throw new APIError("BAD_REQUEST", {
957
- message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
958
- });
959
- }
971
+ await ctx.context.adapter.update({
972
+ model: "subscription",
973
+ update: {
974
+ cancelAtPeriodEnd: false,
975
+ cancelAt: null,
976
+ canceledAt: null,
977
+ updatedAt: new Date(),
978
+ },
979
+ where: [
980
+ {
981
+ field: "id",
982
+ value: subscription.id,
983
+ },
984
+ ],
985
+ });
986
+
987
+ return ctx.json(newSub);
960
988
  },
961
989
  );
962
990
  };
@@ -1031,9 +1059,7 @@ export const listActiveSubscriptions = (options: StripeOptions) => {
1031
1059
  priceId: plan?.priceId,
1032
1060
  };
1033
1061
  })
1034
- .filter((sub) => {
1035
- return sub.status === "active" || sub.status === "trialing";
1036
- });
1062
+ .filter((sub) => isActiveOrTrialing(sub));
1037
1063
  return ctx.json(subs);
1038
1064
  },
1039
1065
  );
@@ -1119,6 +1145,13 @@ export const subscriptionSuccess = (options: StripeOptions) => {
1119
1145
  1000,
1120
1146
  ),
1121
1147
  stripeSubscriptionId: stripeSubscription.id,
1148
+ cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
1149
+ cancelAt: stripeSubscription.cancel_at
1150
+ ? new Date(stripeSubscription.cancel_at * 1000)
1151
+ : null,
1152
+ canceledAt: stripeSubscription.canceled_at
1153
+ ? new Date(stripeSubscription.canceled_at * 1000)
1154
+ : null,
1122
1155
  ...(stripeSubscription.trial_start &&
1123
1156
  stripeSubscription.trial_end
1124
1157
  ? {
@@ -1158,6 +1191,16 @@ const createBillingPortalBodySchema = z.object({
1158
1191
  .optional(),
1159
1192
  referenceId: z.string().optional(),
1160
1193
  returnUrl: z.string().default("/"),
1194
+ /**
1195
+ * Disable Redirect
1196
+ */
1197
+ disableRedirect: z
1198
+ .boolean()
1199
+ .meta({
1200
+ description:
1201
+ "Disable redirect after creating billing portal session. Eg: true",
1202
+ })
1203
+ .default(false),
1161
1204
  });
1162
1205
 
1163
1206
  export const createBillingPortal = (options: StripeOptions) => {
@@ -1196,11 +1239,7 @@ export const createBillingPortal = (options: StripeOptions) => {
1196
1239
  },
1197
1240
  ],
1198
1241
  })
1199
- .then((subs) =>
1200
- subs.find(
1201
- (sub) => sub.status === "active" || sub.status === "trialing",
1202
- ),
1203
- );
1242
+ .then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
1204
1243
 
1205
1244
  customerId = subscription?.stripeCustomerId;
1206
1245
  }
@@ -1220,7 +1259,7 @@ export const createBillingPortal = (options: StripeOptions) => {
1220
1259
 
1221
1260
  return ctx.json({
1222
1261
  url,
1223
- redirect: true,
1262
+ redirect: !ctx.body.disableRedirect,
1224
1263
  });
1225
1264
  } catch (error: any) {
1226
1265
  ctx.context.logger.error(
@@ -1294,6 +1333,10 @@ export const stripeWebhook = (options: StripeOptions) => {
1294
1333
  await onCheckoutSessionCompleted(ctx, options, event);
1295
1334
  await options.onEvent?.(event);
1296
1335
  break;
1336
+ case "customer.subscription.created":
1337
+ await onSubscriptionCreated(ctx, options, event);
1338
+ await options.onEvent?.(event);
1339
+ break;
1297
1340
  case "customer.subscription.updated":
1298
1341
  await onSubscriptionUpdated(ctx, options, event);
1299
1342
  await options.onEvent?.(event);
package/src/schema.ts CHANGED
@@ -46,6 +46,18 @@ export const subscriptions = {
46
46
  required: false,
47
47
  defaultValue: false,
48
48
  },
49
+ cancelAt: {
50
+ type: "date",
51
+ required: false,
52
+ },
53
+ canceledAt: {
54
+ type: "date",
55
+ required: false,
56
+ },
57
+ endedAt: {
58
+ type: "date",
59
+ required: false,
60
+ },
49
61
  seats: {
50
62
  type: "number",
51
63
  required: false,