@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.
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 * as z from "zod/v4";
13
13
  import { STRIPE_ERROR_CODES } from "./error-codes";
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 upgradeSubscriptionBodySchema = z.object({
29
37
  /**
@@ -263,19 +271,15 @@ export const upgradeSubscription = (options: StripeOptions) => {
263
271
  ],
264
272
  });
265
273
 
266
- const activeOrTrialingSubscription = subscriptions.find(
267
- (sub) => sub.status === "active" || sub.status === "trialing",
274
+ const activeOrTrialingSubscription = subscriptions.find((sub) =>
275
+ isActiveOrTrialing(sub),
268
276
  );
269
277
 
270
278
  const activeSubscriptions = await client.subscriptions
271
279
  .list({
272
280
  customer: customerId,
273
281
  })
274
- .then((res) =>
275
- res.data.filter(
276
- (sub) => sub.status === "active" || sub.status === "trialing",
277
- ),
278
- );
282
+ .then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
279
283
 
280
284
  const activeSubscription = activeSubscriptions.find((sub) => {
281
285
  // If we have a specific subscription to update, match by ID
@@ -400,7 +404,7 @@ export const upgradeSubscription = (options: StripeOptions) => {
400
404
  });
401
405
  return ctx.json({
402
406
  url,
403
- redirect: true,
407
+ redirect: !ctx.body.disableRedirect,
404
408
  });
405
409
  }
406
410
 
@@ -457,7 +461,13 @@ export const upgradeSubscription = (options: StripeOptions) => {
457
461
  ctx,
458
462
  );
459
463
 
460
- const hasEverTrialed = subscriptions.some((s) => {
464
+ const allSubscriptions = await ctx.context.adapter.findMany<Subscription>(
465
+ {
466
+ model: "subscription",
467
+ where: [{ field: "referenceId", value: referenceId }],
468
+ },
469
+ );
470
+ const hasEverTrialed = allSubscriptions.some((s) => {
461
471
  // Check if user has ever had a trial for any plan (not just the same plan)
462
472
  // This prevents users from getting multiple trials by switching plans
463
473
  const hadTrial =
@@ -591,8 +601,8 @@ export const cancelSubscriptionCallback = (options: StripeOptions) => {
591
601
  });
592
602
  if (
593
603
  !subscription ||
594
- subscription.cancelAtPeriodEnd ||
595
- subscription.status === "canceled"
604
+ subscription.status === "canceled" ||
605
+ isPendingCancel(subscription)
596
606
  ) {
597
607
  throw ctx.redirect(getUrl(ctx, callbackURL));
598
608
  }
@@ -604,12 +614,24 @@ export const cancelSubscriptionCallback = (options: StripeOptions) => {
604
614
  const currentSubscription = stripeSubscription.data.find(
605
615
  (sub) => sub.id === subscription.stripeSubscriptionId,
606
616
  );
607
- if (currentSubscription?.cancel_at_period_end === true) {
617
+
618
+ const isNewCancellation =
619
+ currentSubscription &&
620
+ isStripePendingCancel(currentSubscription) &&
621
+ !isPendingCancel(subscription);
622
+ if (isNewCancellation) {
608
623
  await ctx.context.adapter.update({
609
624
  model: "subscription",
610
625
  update: {
611
626
  status: currentSubscription?.status,
612
- cancelAtPeriodEnd: true,
627
+ cancelAtPeriodEnd:
628
+ currentSubscription?.cancel_at_period_end || false,
629
+ cancelAt: currentSubscription?.cancel_at
630
+ ? new Date(currentSubscription.cancel_at * 1000)
631
+ : null,
632
+ canceledAt: currentSubscription?.canceled_at
633
+ ? new Date(currentSubscription.canceled_at * 1000)
634
+ : null,
613
635
  },
614
636
  where: [
615
637
  {
@@ -655,6 +677,16 @@ const cancelSubscriptionBodySchema = z.object({
655
677
  description:
656
678
  'URL to take customers to when they click on the billing portal\'s link to return to your website. Eg: "/account"',
657
679
  }),
680
+ /**
681
+ * Disable Redirect
682
+ */
683
+ disableRedirect: z
684
+ .boolean()
685
+ .meta({
686
+ description:
687
+ "Disable redirect after successful subscription cancellation. Eg: true",
688
+ })
689
+ .default(false),
658
690
  });
659
691
 
660
692
  /**
@@ -708,13 +740,7 @@ export const cancelSubscription = (options: StripeOptions) => {
708
740
  model: "subscription",
709
741
  where: [{ field: "referenceId", value: referenceId }],
710
742
  })
711
- .then((subs) =>
712
- subs.find(
713
- (sub) => sub.status === "active" || sub.status === "trialing",
714
- ),
715
- );
716
-
717
- // Ensure the specified subscription belongs to the (validated) referenceId.
743
+ .then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
718
744
  if (
719
745
  ctx.body.subscriptionId &&
720
746
  subscription &&
@@ -733,11 +759,7 @@ export const cancelSubscription = (options: StripeOptions) => {
733
759
  .list({
734
760
  customer: subscription.stripeCustomerId,
735
761
  })
736
- .then((res) =>
737
- res.data.filter(
738
- (sub) => sub.status === "active" || sub.status === "trialing",
739
- ),
740
- );
762
+ .then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
741
763
  if (!activeSubscriptions.length) {
742
764
  /**
743
765
  * If the subscription is not found, we need to delete the subscription
@@ -785,21 +807,30 @@ export const cancelSubscription = (options: StripeOptions) => {
785
807
  },
786
808
  })
787
809
  .catch(async (e) => {
788
- if (e.message.includes("already set to be cancel")) {
810
+ if (e.message?.includes("already set to be canceled")) {
789
811
  /**
790
- * in-case we missed the event from stripe, we set it manually
812
+ * in-case we missed the event from stripe, we sync the actual state
791
813
  * this is a rare case and should not happen
792
814
  */
793
- if (!subscription.cancelAtPeriodEnd) {
794
- await ctx.context.adapter.updateMany({
815
+ if (!isPendingCancel(subscription)) {
816
+ const stripeSub = await client.subscriptions.retrieve(
817
+ activeSubscription.id,
818
+ );
819
+ await ctx.context.adapter.update({
795
820
  model: "subscription",
796
821
  update: {
797
- cancelAtPeriodEnd: true,
822
+ cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
823
+ cancelAt: stripeSub.cancel_at
824
+ ? new Date(stripeSub.cancel_at * 1000)
825
+ : null,
826
+ canceledAt: stripeSub.canceled_at
827
+ ? new Date(stripeSub.canceled_at * 1000)
828
+ : null,
798
829
  },
799
830
  where: [
800
831
  {
801
- field: "referenceId",
802
- value: referenceId,
832
+ field: "id",
833
+ value: subscription.id,
803
834
  },
804
835
  ],
805
836
  });
@@ -810,10 +841,10 @@ export const cancelSubscription = (options: StripeOptions) => {
810
841
  code: e.code,
811
842
  });
812
843
  });
813
- return {
844
+ return ctx.json({
814
845
  url,
815
- redirect: true,
816
- };
846
+ redirect: !ctx.body.disableRedirect,
847
+ });
817
848
  },
818
849
  );
819
850
  };
@@ -875,11 +906,7 @@ export const restoreSubscription = (options: StripeOptions) => {
875
906
  },
876
907
  ],
877
908
  })
878
- .then((subs) =>
879
- subs.find(
880
- (sub) => sub.status === "active" || sub.status === "trialing",
881
- ),
882
- );
909
+ .then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
883
910
  if (
884
911
  ctx.body.subscriptionId &&
885
912
  subscription &&
@@ -902,7 +929,7 @@ export const restoreSubscription = (options: StripeOptions) => {
902
929
  STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
903
930
  );
904
931
  }
905
- if (!subscription.cancelAtPeriodEnd) {
932
+ if (!isPendingCancel(subscription)) {
906
933
  throw APIError.from(
907
934
  "BAD_REQUEST",
908
935
  STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION,
@@ -913,12 +940,7 @@ export const restoreSubscription = (options: StripeOptions) => {
913
940
  .list({
914
941
  customer: subscription.stripeCustomerId,
915
942
  })
916
- .then(
917
- (res) =>
918
- res.data.filter(
919
- (sub) => sub.status === "active" || sub.status === "trialing",
920
- )[0],
921
- );
943
+ .then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
922
944
  if (!activeSubscription) {
923
945
  throw APIError.from(
924
946
  "BAD_REQUEST",
@@ -926,36 +948,41 @@ export const restoreSubscription = (options: StripeOptions) => {
926
948
  );
927
949
  }
928
950
 
929
- try {
930
- const newSub = await client.subscriptions.update(
931
- activeSubscription.id,
932
- {
933
- cancel_at_period_end: false,
934
- },
935
- );
951
+ // Clear scheduled cancellation based on Stripe subscription state
952
+ // Note: Stripe doesn't accept both `cancel_at` and `cancel_at_period_end` simultaneously
953
+ const updateParams: Stripe.SubscriptionUpdateParams = {};
954
+ if (activeSubscription.cancel_at) {
955
+ updateParams.cancel_at = "";
956
+ } else if (activeSubscription.cancel_at_period_end) {
957
+ updateParams.cancel_at_period_end = false;
958
+ }
936
959
 
937
- await ctx.context.adapter.update({
938
- model: "subscription",
939
- update: {
940
- cancelAtPeriodEnd: false,
941
- updatedAt: new Date(),
942
- },
943
- where: [
944
- {
945
- field: "id",
946
- value: subscription.id,
947
- },
948
- ],
960
+ const newSub = await client.subscriptions
961
+ .update(activeSubscription.id, updateParams)
962
+ .catch((e) => {
963
+ throw ctx.error("BAD_REQUEST", {
964
+ message: e.message,
965
+ code: e.code,
966
+ });
949
967
  });
950
968
 
951
- return ctx.json(newSub);
952
- } catch (error) {
953
- ctx.context.logger.error("Error restoring subscription", error);
954
- throw APIError.from(
955
- "BAD_REQUEST",
956
- STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
957
- );
958
- }
969
+ await ctx.context.adapter.update({
970
+ model: "subscription",
971
+ update: {
972
+ cancelAtPeriodEnd: false,
973
+ cancelAt: null,
974
+ canceledAt: null,
975
+ updatedAt: new Date(),
976
+ },
977
+ where: [
978
+ {
979
+ field: "id",
980
+ value: subscription.id,
981
+ },
982
+ ],
983
+ });
984
+
985
+ return ctx.json(newSub);
959
986
  },
960
987
  );
961
988
  };
@@ -1030,9 +1057,7 @@ export const listActiveSubscriptions = (options: StripeOptions) => {
1030
1057
  priceId: plan?.priceId,
1031
1058
  };
1032
1059
  })
1033
- .filter((sub) => {
1034
- return sub.status === "active" || sub.status === "trialing";
1035
- });
1060
+ .filter((sub) => isActiveOrTrialing(sub));
1036
1061
  return ctx.json(subs);
1037
1062
  },
1038
1063
  );
@@ -1118,6 +1143,13 @@ export const subscriptionSuccess = (options: StripeOptions) => {
1118
1143
  1000,
1119
1144
  ),
1120
1145
  stripeSubscriptionId: stripeSubscription.id,
1146
+ cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
1147
+ cancelAt: stripeSubscription.cancel_at
1148
+ ? new Date(stripeSubscription.cancel_at * 1000)
1149
+ : null,
1150
+ canceledAt: stripeSubscription.canceled_at
1151
+ ? new Date(stripeSubscription.canceled_at * 1000)
1152
+ : null,
1121
1153
  ...(stripeSubscription.trial_start &&
1122
1154
  stripeSubscription.trial_end
1123
1155
  ? {
@@ -1157,6 +1189,16 @@ const createBillingPortalBodySchema = z.object({
1157
1189
  .optional(),
1158
1190
  referenceId: z.string().optional(),
1159
1191
  returnUrl: z.string().default("/"),
1192
+ /**
1193
+ * Disable Redirect
1194
+ */
1195
+ disableRedirect: z
1196
+ .boolean()
1197
+ .meta({
1198
+ description:
1199
+ "Disable redirect after creating billing portal session. Eg: true",
1200
+ })
1201
+ .default(false),
1160
1202
  });
1161
1203
 
1162
1204
  export const createBillingPortal = (options: StripeOptions) => {
@@ -1195,11 +1237,7 @@ export const createBillingPortal = (options: StripeOptions) => {
1195
1237
  },
1196
1238
  ],
1197
1239
  })
1198
- .then((subs) =>
1199
- subs.find(
1200
- (sub) => sub.status === "active" || sub.status === "trialing",
1201
- ),
1202
- );
1240
+ .then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
1203
1241
 
1204
1242
  customerId = subscription?.stripeCustomerId;
1205
1243
  }
@@ -1219,7 +1257,7 @@ export const createBillingPortal = (options: StripeOptions) => {
1219
1257
 
1220
1258
  return ctx.json({
1221
1259
  url,
1222
- redirect: true,
1260
+ redirect: !ctx.body.disableRedirect,
1223
1261
  });
1224
1262
  } catch (error: any) {
1225
1263
  ctx.context.logger.error(
@@ -1293,6 +1331,10 @@ export const stripeWebhook = (options: StripeOptions) => {
1293
1331
  await onCheckoutSessionCompleted(ctx, options, event);
1294
1332
  await options.onEvent?.(event);
1295
1333
  break;
1334
+ case "customer.subscription.created":
1335
+ await onSubscriptionCreated(ctx, options, event);
1336
+ await options.onEvent?.(event);
1337
+ break;
1296
1338
  case "customer.subscription.updated":
1297
1339
  await onSubscriptionUpdated(ctx, options, event);
1298
1340
  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,