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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,15 @@
1
1
 
2
- > @better-auth/stripe@1.2.2-beta.3 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.2.2-beta.5 build /home/runner/work/better-auth/better-auth/packages/stripe
3
3
  > unbuild
4
4
 
5
5
  [info] Automatically detected entries: src/index, src/client [esm] [cjs] [dts]
6
6
  [info] Building stripe
7
7
  [success] Build succeeded for stripe
8
- [log] dist/index.cjs (total size: 30.4 kB, chunk size: 30.4 kB, exports: stripe)
8
+ [log] dist/index.cjs (total size: 30.7 kB, chunk size: 30.7 kB, exports: stripe)
9
9
 
10
10
  [log] dist/client.cjs (total size: 160 B, chunk size: 160 B, exports: stripeClient)
11
11
 
12
- [log] dist/index.mjs (total size: 30.2 kB, chunk size: 30.2 kB, exports: stripe)
12
+ [log] dist/index.mjs (total size: 30.5 kB, chunk size: 30.5 kB, exports: stripe)
13
13
 
14
14
  [log] dist/client.mjs (total size: 133 B, chunk size: 133 B, exports: stripeClient)
15
15
 
package/dist/index.cjs CHANGED
@@ -51,6 +51,7 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
51
51
  periodEnd: new Date(subscription.current_period_end * 1e3),
52
52
  stripeSubscriptionId: checkoutSession.subscription,
53
53
  seats,
54
+ stripeCustomerId: subscription.customer.toString(),
54
55
  ...trial
55
56
  },
56
57
  where: [
@@ -127,6 +128,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
127
128
  periodStart: new Date(subscriptionUpdated.current_period_start * 1e3),
128
129
  periodEnd: new Date(subscriptionUpdated.current_period_end * 1e3),
129
130
  cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
131
+ stripeCustomerId: subscriptionUpdated.customer.toString(),
130
132
  seats,
131
133
  stripeSubscriptionId: subscriptionUpdated.id
132
134
  },
@@ -175,40 +177,38 @@ async function onSubscriptionDeleted(ctx, options, event) {
175
177
  try {
176
178
  const subscriptionDeleted = event.data.object;
177
179
  const subscriptionId = subscriptionDeleted.id;
178
- if (subscriptionDeleted.status === "canceled") {
179
- const subscription = await ctx.context.adapter.findOne({
180
+ const subscription = await ctx.context.adapter.findOne({
181
+ model: "subscription",
182
+ where: [
183
+ {
184
+ field: "stripeSubscriptionId",
185
+ value: subscriptionId
186
+ }
187
+ ]
188
+ });
189
+ if (subscription) {
190
+ await ctx.context.adapter.update({
180
191
  model: "subscription",
181
192
  where: [
182
193
  {
183
194
  field: "stripeSubscriptionId",
184
195
  value: subscriptionId
185
196
  }
186
- ]
197
+ ],
198
+ update: {
199
+ status: "canceled",
200
+ updatedAt: /* @__PURE__ */ new Date()
201
+ }
187
202
  });
188
- if (subscription) {
189
- await ctx.context.adapter.update({
190
- model: "subscription",
191
- where: [
192
- {
193
- field: "stripeSubscriptionId",
194
- value: subscriptionId
195
- }
196
- ],
197
- update: {
198
- status: "canceled",
199
- updatedAt: /* @__PURE__ */ new Date()
200
- }
201
- });
202
- await options.subscription.onSubscriptionDeleted?.({
203
- event,
204
- stripeSubscription: subscriptionDeleted,
205
- subscription
206
- });
207
- } else {
208
- betterAuth.logger.warn(
209
- `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`
210
- );
211
- }
203
+ await options.subscription.onSubscriptionDeleted?.({
204
+ event,
205
+ stripeSubscription: subscriptionDeleted,
206
+ subscription
207
+ });
208
+ } else {
209
+ betterAuth.logger.warn(
210
+ `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`
211
+ );
212
212
  }
213
213
  } catch (error) {
214
214
  betterAuth.logger.error(`Stripe webhook failed. Error: ${error}`);
@@ -314,7 +314,12 @@ const stripe = (options) => {
314
314
  {
315
315
  method: "POST",
316
316
  body: zod.z.object({
317
- plan: zod.z.string(),
317
+ plan: zod.z.string({
318
+ description: "The name of the plan to upgrade to"
319
+ }),
320
+ annual: zod.z.boolean({
321
+ description: "Whether to upgrade to an annual plan"
322
+ }).optional(),
318
323
  referenceId: zod.z.string().optional(),
319
324
  metadata: zod.z.record(zod.z.string(), zod.z.any()).optional(),
320
325
  seats: zod.z.number({
@@ -328,7 +333,6 @@ const stripe = (options) => {
328
333
  description: "callback url to redirect back after successful subscription"
329
334
  }).default("/"),
330
335
  returnUrl: zod.z.string().optional(),
331
- withoutTrial: zod.z.boolean().optional(),
332
336
  disableRedirect: zod.z.boolean().default(false)
333
337
  }),
334
338
  use: [
@@ -405,6 +409,11 @@ const stripe = (options) => {
405
409
  const existingSubscription = subscriptions.find(
406
410
  (sub) => sub.status === "active" || sub.status === "trialing"
407
411
  );
412
+ if (existingSubscription && existingSubscription.status === "active" && existingSubscription.plan === ctx.body.plan) {
413
+ throw new api.APIError("BAD_REQUEST", {
414
+ message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
415
+ });
416
+ }
408
417
  if (activeSubscription && customerId) {
409
418
  const { url } = await client.billingPortal.sessions.create({
410
419
  customer: customerId,
@@ -417,35 +426,12 @@ const stripe = (options) => {
417
426
  {
418
427
  id: activeSubscription.items.data[0]?.id,
419
428
  quantity: 1,
420
- price: plan.priceId
429
+ price: ctx.body.annual ? plan.annualDiscountPriceId : plan.priceId
421
430
  }
422
431
  ]
423
432
  }
424
433
  }
425
434
  }).catch(async (e) => {
426
- if (e.message.includes("no changes")) {
427
- const plan2 = await getPlanByPriceId(
428
- options,
429
- activeSubscription.items.data[0]?.plan.id
430
- );
431
- await ctx.context.adapter.update({
432
- model: "subscription",
433
- update: {
434
- status: activeSubscription.status,
435
- seats: activeSubscription.items.data[0]?.quantity,
436
- plan: plan2?.name.toLowerCase()
437
- },
438
- where: [
439
- {
440
- field: "referenceId",
441
- value: referenceId
442
- }
443
- ]
444
- });
445
- throw new api.APIError("BAD_REQUEST", {
446
- message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
447
- });
448
- }
449
435
  throw ctx.error("BAD_REQUEST", {
450
436
  message: e.message,
451
437
  code: e.code
@@ -456,11 +442,6 @@ const stripe = (options) => {
456
442
  redirect: true
457
443
  });
458
444
  }
459
- if (existingSubscription && existingSubscription.status === "active" && existingSubscription.plan === ctx.body.plan) {
460
- throw new api.APIError("BAD_REQUEST", {
461
- message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
462
- });
463
- }
464
445
  let subscription = existingSubscription;
465
446
  if (!subscription) {
466
447
  const newSubscription = await ctx.context.adapter.create({
@@ -488,39 +469,48 @@ const stripe = (options) => {
488
469
  },
489
470
  ctx.request
490
471
  );
491
- const checkoutSession = await client.checkout.sessions.create({
492
- ...customerId ? {
493
- customer: customerId,
494
- customer_update: {
495
- name: "auto",
496
- address: "auto"
472
+ const freeTrail = plan.freeTrial ? {
473
+ trial_period_days: plan.freeTrial.days
474
+ } : void 0;
475
+ const checkoutSession = await client.checkout.sessions.create(
476
+ {
477
+ ...customerId ? {
478
+ customer: customerId,
479
+ customer_update: {
480
+ name: "auto",
481
+ address: "auto"
482
+ }
483
+ } : {
484
+ customer_email: session.user.email
485
+ },
486
+ success_url: getUrl(
487
+ ctx,
488
+ `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(
489
+ ctx.body.successUrl
490
+ )}&reference=${encodeURIComponent(referenceId)}`
491
+ ),
492
+ cancel_url: getUrl(ctx, ctx.body.cancelUrl),
493
+ line_items: [
494
+ {
495
+ price: ctx.body.annual ? plan.annualDiscountPriceId : plan.priceId,
496
+ quantity: ctx.body.seats || 1
497
+ }
498
+ ],
499
+ subscription_data: {
500
+ ...freeTrail
501
+ },
502
+ mode: "subscription",
503
+ client_reference_id: referenceId,
504
+ ...params?.params,
505
+ metadata: {
506
+ userId: user.id,
507
+ subscriptionId: subscription.id,
508
+ referenceId,
509
+ ...params?.params?.metadata
497
510
  }
498
- } : {
499
- customer_email: session.user.email
500
511
  },
501
- success_url: getUrl(
502
- ctx,
503
- `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(
504
- ctx.body.successUrl
505
- )}&reference=${encodeURIComponent(referenceId)}`
506
- ),
507
- cancel_url: getUrl(ctx, ctx.body.cancelUrl),
508
- line_items: [
509
- {
510
- price: plan.priceId,
511
- quantity: ctx.body.seats || 1
512
- }
513
- ],
514
- mode: "subscription",
515
- client_reference_id: referenceId,
516
- ...params,
517
- metadata: {
518
- userId: user.id,
519
- subscriptionId: subscription.id,
520
- referenceId,
521
- ...params?.params?.metadata
522
- }
523
- }).catch(async (e) => {
512
+ params?.options
513
+ ).catch(async (e) => {
524
514
  throw ctx.error("BAD_REQUEST", {
525
515
  message: e.message,
526
516
  code: e.code
@@ -632,10 +622,30 @@ const stripe = (options) => {
632
622
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
633
623
  });
634
624
  }
635
- const activeSubscription = await client.subscriptions.list({
636
- customer: subscription.stripeCustomerId,
637
- status: "active"
638
- }).then((res) => res.data[0]);
625
+ const activeSubscriptions = await client.subscriptions.list({
626
+ customer: subscription.stripeCustomerId
627
+ }).then(
628
+ (res) => res.data.filter(
629
+ (sub) => sub.status === "active" || sub.status === "trialing"
630
+ )
631
+ );
632
+ if (!activeSubscriptions.length) {
633
+ await ctx.context.adapter.deleteMany({
634
+ model: "subscription",
635
+ where: [
636
+ {
637
+ field: "referenceId",
638
+ value: referenceId
639
+ }
640
+ ]
641
+ });
642
+ throw ctx.error("BAD_REQUEST", {
643
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
644
+ });
645
+ }
646
+ const activeSubscription = activeSubscriptions.find(
647
+ (sub) => sub.id === subscription.stripeSubscriptionId
648
+ );
639
649
  if (!activeSubscription) {
640
650
  throw ctx.error("BAD_REQUEST", {
641
651
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
package/dist/index.d.cts CHANGED
@@ -47,12 +47,6 @@ type Plan = {
47
47
  * Number of days
48
48
  */
49
49
  days: number;
50
- /**
51
- * Only available for new users or users without existing subscription
52
- *
53
- * @default true
54
- */
55
- forNewUsersOnly?: boolean;
56
50
  /**
57
51
  * A function that will be called when the trial
58
52
  * starts.
@@ -335,13 +329,13 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
335
329
  body: {
336
330
  plan: string;
337
331
  metadata?: Record<string, any> | undefined;
332
+ annual?: boolean | undefined;
338
333
  referenceId?: string | undefined;
339
334
  seats?: number | undefined;
340
335
  uiMode?: "embedded" | "hosted" | undefined;
341
336
  successUrl?: string | undefined;
342
337
  cancelUrl?: string | undefined;
343
338
  returnUrl?: string | undefined;
344
- withoutTrial?: boolean | undefined;
345
339
  disableRedirect?: boolean | undefined;
346
340
  };
347
341
  method?: "POST" | undefined;
@@ -511,6 +505,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
511
505
  method: "POST";
512
506
  body: z.ZodObject<{
513
507
  plan: z.ZodString;
508
+ annual: z.ZodOptional<z.ZodBoolean>;
514
509
  referenceId: z.ZodOptional<z.ZodString>;
515
510
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
516
511
  seats: z.ZodOptional<z.ZodNumber>;
@@ -518,7 +513,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
518
513
  successUrl: z.ZodDefault<z.ZodString>;
519
514
  cancelUrl: z.ZodDefault<z.ZodString>;
520
515
  returnUrl: z.ZodOptional<z.ZodString>;
521
- withoutTrial: z.ZodOptional<z.ZodBoolean>;
522
516
  disableRedirect: z.ZodDefault<z.ZodBoolean>;
523
517
  }, "strip", z.ZodTypeAny, {
524
518
  plan: string;
@@ -527,20 +521,20 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
527
521
  cancelUrl: string;
528
522
  disableRedirect: boolean;
529
523
  metadata?: Record<string, any> | undefined;
524
+ annual?: boolean | undefined;
530
525
  referenceId?: string | undefined;
531
526
  seats?: number | undefined;
532
527
  returnUrl?: string | undefined;
533
- withoutTrial?: boolean | undefined;
534
528
  }, {
535
529
  plan: string;
536
530
  metadata?: Record<string, any> | undefined;
531
+ annual?: boolean | undefined;
537
532
  referenceId?: string | undefined;
538
533
  seats?: number | undefined;
539
534
  uiMode?: "embedded" | "hosted" | undefined;
540
535
  successUrl?: string | undefined;
541
536
  cancelUrl?: string | undefined;
542
537
  returnUrl?: string | undefined;
543
- withoutTrial?: boolean | undefined;
544
538
  disableRedirect?: boolean | undefined;
545
539
  }>;
546
540
  use: (((inputContext: {
package/dist/index.d.mts CHANGED
@@ -47,12 +47,6 @@ type Plan = {
47
47
  * Number of days
48
48
  */
49
49
  days: number;
50
- /**
51
- * Only available for new users or users without existing subscription
52
- *
53
- * @default true
54
- */
55
- forNewUsersOnly?: boolean;
56
50
  /**
57
51
  * A function that will be called when the trial
58
52
  * starts.
@@ -335,13 +329,13 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
335
329
  body: {
336
330
  plan: string;
337
331
  metadata?: Record<string, any> | undefined;
332
+ annual?: boolean | undefined;
338
333
  referenceId?: string | undefined;
339
334
  seats?: number | undefined;
340
335
  uiMode?: "embedded" | "hosted" | undefined;
341
336
  successUrl?: string | undefined;
342
337
  cancelUrl?: string | undefined;
343
338
  returnUrl?: string | undefined;
344
- withoutTrial?: boolean | undefined;
345
339
  disableRedirect?: boolean | undefined;
346
340
  };
347
341
  method?: "POST" | undefined;
@@ -511,6 +505,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
511
505
  method: "POST";
512
506
  body: z.ZodObject<{
513
507
  plan: z.ZodString;
508
+ annual: z.ZodOptional<z.ZodBoolean>;
514
509
  referenceId: z.ZodOptional<z.ZodString>;
515
510
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
516
511
  seats: z.ZodOptional<z.ZodNumber>;
@@ -518,7 +513,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
518
513
  successUrl: z.ZodDefault<z.ZodString>;
519
514
  cancelUrl: z.ZodDefault<z.ZodString>;
520
515
  returnUrl: z.ZodOptional<z.ZodString>;
521
- withoutTrial: z.ZodOptional<z.ZodBoolean>;
522
516
  disableRedirect: z.ZodDefault<z.ZodBoolean>;
523
517
  }, "strip", z.ZodTypeAny, {
524
518
  plan: string;
@@ -527,20 +521,20 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
527
521
  cancelUrl: string;
528
522
  disableRedirect: boolean;
529
523
  metadata?: Record<string, any> | undefined;
524
+ annual?: boolean | undefined;
530
525
  referenceId?: string | undefined;
531
526
  seats?: number | undefined;
532
527
  returnUrl?: string | undefined;
533
- withoutTrial?: boolean | undefined;
534
528
  }, {
535
529
  plan: string;
536
530
  metadata?: Record<string, any> | undefined;
531
+ annual?: boolean | undefined;
537
532
  referenceId?: string | undefined;
538
533
  seats?: number | undefined;
539
534
  uiMode?: "embedded" | "hosted" | undefined;
540
535
  successUrl?: string | undefined;
541
536
  cancelUrl?: string | undefined;
542
537
  returnUrl?: string | undefined;
543
- withoutTrial?: boolean | undefined;
544
538
  disableRedirect?: boolean | undefined;
545
539
  }>;
546
540
  use: (((inputContext: {
package/dist/index.d.ts CHANGED
@@ -47,12 +47,6 @@ type Plan = {
47
47
  * Number of days
48
48
  */
49
49
  days: number;
50
- /**
51
- * Only available for new users or users without existing subscription
52
- *
53
- * @default true
54
- */
55
- forNewUsersOnly?: boolean;
56
50
  /**
57
51
  * A function that will be called when the trial
58
52
  * starts.
@@ -335,13 +329,13 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
335
329
  body: {
336
330
  plan: string;
337
331
  metadata?: Record<string, any> | undefined;
332
+ annual?: boolean | undefined;
338
333
  referenceId?: string | undefined;
339
334
  seats?: number | undefined;
340
335
  uiMode?: "embedded" | "hosted" | undefined;
341
336
  successUrl?: string | undefined;
342
337
  cancelUrl?: string | undefined;
343
338
  returnUrl?: string | undefined;
344
- withoutTrial?: boolean | undefined;
345
339
  disableRedirect?: boolean | undefined;
346
340
  };
347
341
  method?: "POST" | undefined;
@@ -511,6 +505,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
511
505
  method: "POST";
512
506
  body: z.ZodObject<{
513
507
  plan: z.ZodString;
508
+ annual: z.ZodOptional<z.ZodBoolean>;
514
509
  referenceId: z.ZodOptional<z.ZodString>;
515
510
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
516
511
  seats: z.ZodOptional<z.ZodNumber>;
@@ -518,7 +513,6 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
518
513
  successUrl: z.ZodDefault<z.ZodString>;
519
514
  cancelUrl: z.ZodDefault<z.ZodString>;
520
515
  returnUrl: z.ZodOptional<z.ZodString>;
521
- withoutTrial: z.ZodOptional<z.ZodBoolean>;
522
516
  disableRedirect: z.ZodDefault<z.ZodBoolean>;
523
517
  }, "strip", z.ZodTypeAny, {
524
518
  plan: string;
@@ -527,20 +521,20 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
527
521
  cancelUrl: string;
528
522
  disableRedirect: boolean;
529
523
  metadata?: Record<string, any> | undefined;
524
+ annual?: boolean | undefined;
530
525
  referenceId?: string | undefined;
531
526
  seats?: number | undefined;
532
527
  returnUrl?: string | undefined;
533
- withoutTrial?: boolean | undefined;
534
528
  }, {
535
529
  plan: string;
536
530
  metadata?: Record<string, any> | undefined;
531
+ annual?: boolean | undefined;
537
532
  referenceId?: string | undefined;
538
533
  seats?: number | undefined;
539
534
  uiMode?: "embedded" | "hosted" | undefined;
540
535
  successUrl?: string | undefined;
541
536
  cancelUrl?: string | undefined;
542
537
  returnUrl?: string | undefined;
543
- withoutTrial?: boolean | undefined;
544
538
  disableRedirect?: boolean | undefined;
545
539
  }>;
546
540
  use: (((inputContext: {
package/dist/index.mjs CHANGED
@@ -49,6 +49,7 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
49
49
  periodEnd: new Date(subscription.current_period_end * 1e3),
50
50
  stripeSubscriptionId: checkoutSession.subscription,
51
51
  seats,
52
+ stripeCustomerId: subscription.customer.toString(),
52
53
  ...trial
53
54
  },
54
55
  where: [
@@ -125,6 +126,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
125
126
  periodStart: new Date(subscriptionUpdated.current_period_start * 1e3),
126
127
  periodEnd: new Date(subscriptionUpdated.current_period_end * 1e3),
127
128
  cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
129
+ stripeCustomerId: subscriptionUpdated.customer.toString(),
128
130
  seats,
129
131
  stripeSubscriptionId: subscriptionUpdated.id
130
132
  },
@@ -173,40 +175,38 @@ async function onSubscriptionDeleted(ctx, options, event) {
173
175
  try {
174
176
  const subscriptionDeleted = event.data.object;
175
177
  const subscriptionId = subscriptionDeleted.id;
176
- if (subscriptionDeleted.status === "canceled") {
177
- const subscription = await ctx.context.adapter.findOne({
178
+ const subscription = await ctx.context.adapter.findOne({
179
+ model: "subscription",
180
+ where: [
181
+ {
182
+ field: "stripeSubscriptionId",
183
+ value: subscriptionId
184
+ }
185
+ ]
186
+ });
187
+ if (subscription) {
188
+ await ctx.context.adapter.update({
178
189
  model: "subscription",
179
190
  where: [
180
191
  {
181
192
  field: "stripeSubscriptionId",
182
193
  value: subscriptionId
183
194
  }
184
- ]
195
+ ],
196
+ update: {
197
+ status: "canceled",
198
+ updatedAt: /* @__PURE__ */ new Date()
199
+ }
185
200
  });
186
- if (subscription) {
187
- await ctx.context.adapter.update({
188
- model: "subscription",
189
- where: [
190
- {
191
- field: "stripeSubscriptionId",
192
- value: subscriptionId
193
- }
194
- ],
195
- update: {
196
- status: "canceled",
197
- updatedAt: /* @__PURE__ */ new Date()
198
- }
199
- });
200
- await options.subscription.onSubscriptionDeleted?.({
201
- event,
202
- stripeSubscription: subscriptionDeleted,
203
- subscription
204
- });
205
- } else {
206
- logger.warn(
207
- `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`
208
- );
209
- }
201
+ await options.subscription.onSubscriptionDeleted?.({
202
+ event,
203
+ stripeSubscription: subscriptionDeleted,
204
+ subscription
205
+ });
206
+ } else {
207
+ logger.warn(
208
+ `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`
209
+ );
210
210
  }
211
211
  } catch (error) {
212
212
  logger.error(`Stripe webhook failed. Error: ${error}`);
@@ -312,7 +312,12 @@ const stripe = (options) => {
312
312
  {
313
313
  method: "POST",
314
314
  body: z.object({
315
- plan: z.string(),
315
+ plan: z.string({
316
+ description: "The name of the plan to upgrade to"
317
+ }),
318
+ annual: z.boolean({
319
+ description: "Whether to upgrade to an annual plan"
320
+ }).optional(),
316
321
  referenceId: z.string().optional(),
317
322
  metadata: z.record(z.string(), z.any()).optional(),
318
323
  seats: z.number({
@@ -326,7 +331,6 @@ const stripe = (options) => {
326
331
  description: "callback url to redirect back after successful subscription"
327
332
  }).default("/"),
328
333
  returnUrl: z.string().optional(),
329
- withoutTrial: z.boolean().optional(),
330
334
  disableRedirect: z.boolean().default(false)
331
335
  }),
332
336
  use: [
@@ -403,6 +407,11 @@ const stripe = (options) => {
403
407
  const existingSubscription = subscriptions.find(
404
408
  (sub) => sub.status === "active" || sub.status === "trialing"
405
409
  );
410
+ if (existingSubscription && existingSubscription.status === "active" && existingSubscription.plan === ctx.body.plan) {
411
+ throw new APIError("BAD_REQUEST", {
412
+ message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
413
+ });
414
+ }
406
415
  if (activeSubscription && customerId) {
407
416
  const { url } = await client.billingPortal.sessions.create({
408
417
  customer: customerId,
@@ -415,35 +424,12 @@ const stripe = (options) => {
415
424
  {
416
425
  id: activeSubscription.items.data[0]?.id,
417
426
  quantity: 1,
418
- price: plan.priceId
427
+ price: ctx.body.annual ? plan.annualDiscountPriceId : plan.priceId
419
428
  }
420
429
  ]
421
430
  }
422
431
  }
423
432
  }).catch(async (e) => {
424
- if (e.message.includes("no changes")) {
425
- const plan2 = await getPlanByPriceId(
426
- options,
427
- activeSubscription.items.data[0]?.plan.id
428
- );
429
- await ctx.context.adapter.update({
430
- model: "subscription",
431
- update: {
432
- status: activeSubscription.status,
433
- seats: activeSubscription.items.data[0]?.quantity,
434
- plan: plan2?.name.toLowerCase()
435
- },
436
- where: [
437
- {
438
- field: "referenceId",
439
- value: referenceId
440
- }
441
- ]
442
- });
443
- throw new APIError("BAD_REQUEST", {
444
- message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
445
- });
446
- }
447
433
  throw ctx.error("BAD_REQUEST", {
448
434
  message: e.message,
449
435
  code: e.code
@@ -454,11 +440,6 @@ const stripe = (options) => {
454
440
  redirect: true
455
441
  });
456
442
  }
457
- if (existingSubscription && existingSubscription.status === "active" && existingSubscription.plan === ctx.body.plan) {
458
- throw new APIError("BAD_REQUEST", {
459
- message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
460
- });
461
- }
462
443
  let subscription = existingSubscription;
463
444
  if (!subscription) {
464
445
  const newSubscription = await ctx.context.adapter.create({
@@ -486,39 +467,48 @@ const stripe = (options) => {
486
467
  },
487
468
  ctx.request
488
469
  );
489
- const checkoutSession = await client.checkout.sessions.create({
490
- ...customerId ? {
491
- customer: customerId,
492
- customer_update: {
493
- name: "auto",
494
- address: "auto"
470
+ const freeTrail = plan.freeTrial ? {
471
+ trial_period_days: plan.freeTrial.days
472
+ } : void 0;
473
+ const checkoutSession = await client.checkout.sessions.create(
474
+ {
475
+ ...customerId ? {
476
+ customer: customerId,
477
+ customer_update: {
478
+ name: "auto",
479
+ address: "auto"
480
+ }
481
+ } : {
482
+ customer_email: session.user.email
483
+ },
484
+ success_url: getUrl(
485
+ ctx,
486
+ `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(
487
+ ctx.body.successUrl
488
+ )}&reference=${encodeURIComponent(referenceId)}`
489
+ ),
490
+ cancel_url: getUrl(ctx, ctx.body.cancelUrl),
491
+ line_items: [
492
+ {
493
+ price: ctx.body.annual ? plan.annualDiscountPriceId : plan.priceId,
494
+ quantity: ctx.body.seats || 1
495
+ }
496
+ ],
497
+ subscription_data: {
498
+ ...freeTrail
499
+ },
500
+ mode: "subscription",
501
+ client_reference_id: referenceId,
502
+ ...params?.params,
503
+ metadata: {
504
+ userId: user.id,
505
+ subscriptionId: subscription.id,
506
+ referenceId,
507
+ ...params?.params?.metadata
495
508
  }
496
- } : {
497
- customer_email: session.user.email
498
509
  },
499
- success_url: getUrl(
500
- ctx,
501
- `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(
502
- ctx.body.successUrl
503
- )}&reference=${encodeURIComponent(referenceId)}`
504
- ),
505
- cancel_url: getUrl(ctx, ctx.body.cancelUrl),
506
- line_items: [
507
- {
508
- price: plan.priceId,
509
- quantity: ctx.body.seats || 1
510
- }
511
- ],
512
- mode: "subscription",
513
- client_reference_id: referenceId,
514
- ...params,
515
- metadata: {
516
- userId: user.id,
517
- subscriptionId: subscription.id,
518
- referenceId,
519
- ...params?.params?.metadata
520
- }
521
- }).catch(async (e) => {
510
+ params?.options
511
+ ).catch(async (e) => {
522
512
  throw ctx.error("BAD_REQUEST", {
523
513
  message: e.message,
524
514
  code: e.code
@@ -630,10 +620,30 @@ const stripe = (options) => {
630
620
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
631
621
  });
632
622
  }
633
- const activeSubscription = await client.subscriptions.list({
634
- customer: subscription.stripeCustomerId,
635
- status: "active"
636
- }).then((res) => res.data[0]);
623
+ const activeSubscriptions = await client.subscriptions.list({
624
+ customer: subscription.stripeCustomerId
625
+ }).then(
626
+ (res) => res.data.filter(
627
+ (sub) => sub.status === "active" || sub.status === "trialing"
628
+ )
629
+ );
630
+ if (!activeSubscriptions.length) {
631
+ await ctx.context.adapter.deleteMany({
632
+ model: "subscription",
633
+ where: [
634
+ {
635
+ field: "referenceId",
636
+ value: referenceId
637
+ }
638
+ ]
639
+ });
640
+ throw ctx.error("BAD_REQUEST", {
641
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
642
+ });
643
+ }
644
+ const activeSubscription = activeSubscriptions.find(
645
+ (sub) => sub.id === subscription.stripeSubscriptionId
646
+ );
637
647
  if (!activeSubscription) {
638
648
  throw ctx.error("BAD_REQUEST", {
639
649
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/stripe",
3
3
  "author": "Bereket Engida",
4
- "version": "1.2.2-beta.3",
4
+ "version": "1.2.2-beta.5",
5
5
  "main": "dist/index.cjs",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "zod": "^3.24.1",
38
- "better-auth": "^1.2.2-beta.3"
38
+ "better-auth": "^1.2.2-beta.5"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/better-sqlite3": "^7.6.12",
package/src/hooks.ts CHANGED
@@ -43,6 +43,7 @@ export async function onCheckoutSessionCompleted(
43
43
  periodEnd: new Date(subscription.current_period_end * 1000),
44
44
  stripeSubscriptionId: checkoutSession.subscription as string,
45
45
  seats,
46
+ stripeCustomerId: subscription.customer.toString(),
46
47
  ...trial,
47
48
  },
48
49
  where: [
@@ -132,6 +133,7 @@ export async function onSubscriptionUpdated(
132
133
  periodStart: new Date(subscriptionUpdated.current_period_start * 1000),
133
134
  periodEnd: new Date(subscriptionUpdated.current_period_end * 1000),
134
135
  cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
136
+ stripeCustomerId: subscriptionUpdated.customer.toString(),
135
137
  seats,
136
138
  stripeSubscriptionId: subscriptionUpdated.id,
137
139
  },
@@ -197,8 +199,17 @@ export async function onSubscriptionDeleted(
197
199
  try {
198
200
  const subscriptionDeleted = event.data.object as Stripe.Subscription;
199
201
  const subscriptionId = subscriptionDeleted.id;
200
- if (subscriptionDeleted.status === "canceled") {
201
- const subscription = await ctx.context.adapter.findOne<Subscription>({
202
+ const subscription = await ctx.context.adapter.findOne<Subscription>({
203
+ model: "subscription",
204
+ where: [
205
+ {
206
+ field: "stripeSubscriptionId",
207
+ value: subscriptionId,
208
+ },
209
+ ],
210
+ });
211
+ if (subscription) {
212
+ await ctx.context.adapter.update({
202
213
  model: "subscription",
203
214
  where: [
204
215
  {
@@ -206,31 +217,20 @@ export async function onSubscriptionDeleted(
206
217
  value: subscriptionId,
207
218
  },
208
219
  ],
220
+ update: {
221
+ status: "canceled",
222
+ updatedAt: new Date(),
223
+ },
209
224
  });
210
- if (subscription) {
211
- await ctx.context.adapter.update({
212
- model: "subscription",
213
- where: [
214
- {
215
- field: "stripeSubscriptionId",
216
- value: subscriptionId,
217
- },
218
- ],
219
- update: {
220
- status: "canceled",
221
- updatedAt: new Date(),
222
- },
223
- });
224
- await options.subscription.onSubscriptionDeleted?.({
225
- event,
226
- stripeSubscription: subscriptionDeleted,
227
- subscription,
228
- });
229
- } else {
230
- logger.warn(
231
- `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
232
- );
233
- }
225
+ await options.subscription.onSubscriptionDeleted?.({
226
+ event,
227
+ stripeSubscription: subscriptionDeleted,
228
+ subscription,
229
+ });
230
+ } else {
231
+ logger.warn(
232
+ `Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
233
+ );
234
234
  }
235
235
  } catch (error: any) {
236
236
  logger.error(`Stripe webhook failed. Error: ${error}`);
package/src/index.ts CHANGED
@@ -77,7 +77,14 @@ export const stripe = <O extends StripeOptions>(options: O) => {
77
77
  {
78
78
  method: "POST",
79
79
  body: z.object({
80
- plan: z.string(),
80
+ plan: z.string({
81
+ description: "The name of the plan to upgrade to",
82
+ }),
83
+ annual: z
84
+ .boolean({
85
+ description: "Whether to upgrade to an annual plan",
86
+ })
87
+ .optional(),
81
88
  referenceId: z.string().optional(),
82
89
  metadata: z.record(z.string(), z.any()).optional(),
83
90
  seats: z
@@ -99,7 +106,6 @@ export const stripe = <O extends StripeOptions>(options: O) => {
99
106
  })
100
107
  .default("/"),
101
108
  returnUrl: z.string().optional(),
102
- withoutTrial: z.boolean().optional(),
103
109
  disableRedirect: z.boolean().default(false),
104
110
  }),
105
111
  use: [
@@ -182,9 +188,21 @@ export const stripe = <O extends StripeOptions>(options: O) => {
182
188
  },
183
189
  ],
184
190
  });
191
+
185
192
  const existingSubscription = subscriptions.find(
186
193
  (sub) => sub.status === "active" || sub.status === "trialing",
187
194
  );
195
+
196
+ if (
197
+ existingSubscription &&
198
+ existingSubscription.status === "active" &&
199
+ existingSubscription.plan === ctx.body.plan
200
+ ) {
201
+ throw new APIError("BAD_REQUEST", {
202
+ message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
203
+ });
204
+ }
205
+
188
206
  if (activeSubscription && customerId) {
189
207
  const { url } = await client.billingPortal.sessions
190
208
  .create({
@@ -198,40 +216,15 @@ export const stripe = <O extends StripeOptions>(options: O) => {
198
216
  {
199
217
  id: activeSubscription.items.data[0]?.id as string,
200
218
  quantity: 1,
201
- price: plan.priceId,
219
+ price: ctx.body.annual
220
+ ? plan.annualDiscountPriceId
221
+ : plan.priceId,
202
222
  },
203
223
  ],
204
224
  },
205
225
  },
206
226
  })
207
227
  .catch(async (e) => {
208
- if (e.message.includes("no changes")) {
209
- /**
210
- * If the subscription is already active on stripe, we need to
211
- * update the status to the new status.
212
- */
213
- const plan = await getPlanByPriceId(
214
- options,
215
- activeSubscription.items.data[0]?.plan.id,
216
- );
217
- await ctx.context.adapter.update({
218
- model: "subscription",
219
- update: {
220
- status: activeSubscription.status,
221
- seats: activeSubscription.items.data[0]?.quantity,
222
- plan: plan?.name.toLowerCase(),
223
- },
224
- where: [
225
- {
226
- field: "referenceId",
227
- value: referenceId,
228
- },
229
- ],
230
- });
231
- throw new APIError("BAD_REQUEST", {
232
- message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
233
- });
234
- }
235
228
  throw ctx.error("BAD_REQUEST", {
236
229
  message: e.message,
237
230
  code: e.code,
@@ -243,15 +236,6 @@ export const stripe = <O extends StripeOptions>(options: O) => {
243
236
  });
244
237
  }
245
238
 
246
- if (
247
- existingSubscription &&
248
- existingSubscription.status === "active" &&
249
- existingSubscription.plan === ctx.body.plan
250
- ) {
251
- throw new APIError("BAD_REQUEST", {
252
- message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
253
- });
254
- }
255
239
  let subscription = existingSubscription;
256
240
  if (!subscription) {
257
241
  const newSubscription = await ctx.context.adapter.create<
@@ -285,44 +269,58 @@ export const stripe = <O extends StripeOptions>(options: O) => {
285
269
  ctx.request,
286
270
  );
287
271
 
272
+ const freeTrail = plan.freeTrial
273
+ ? {
274
+ trial_period_days: plan.freeTrial.days,
275
+ }
276
+ : undefined;
277
+
288
278
  const checkoutSession = await client.checkout.sessions
289
- .create({
290
- ...(customerId
291
- ? {
292
- customer: customerId,
293
- customer_update: {
294
- name: "auto",
295
- address: "auto",
296
- },
297
- }
298
- : {
299
- customer_email: session.user.email,
300
- }),
301
- success_url: getUrl(
302
- ctx,
303
- `${
304
- ctx.context.baseURL
305
- }/subscription/success?callbackURL=${encodeURIComponent(
306
- ctx.body.successUrl,
307
- )}&reference=${encodeURIComponent(referenceId)}`,
308
- ),
309
- cancel_url: getUrl(ctx, ctx.body.cancelUrl),
310
- line_items: [
311
- {
312
- price: plan.priceId,
313
- quantity: ctx.body.seats || 1,
279
+ .create(
280
+ {
281
+ ...(customerId
282
+ ? {
283
+ customer: customerId,
284
+ customer_update: {
285
+ name: "auto",
286
+ address: "auto",
287
+ },
288
+ }
289
+ : {
290
+ customer_email: session.user.email,
291
+ }),
292
+ success_url: getUrl(
293
+ ctx,
294
+ `${
295
+ ctx.context.baseURL
296
+ }/subscription/success?callbackURL=${encodeURIComponent(
297
+ ctx.body.successUrl,
298
+ )}&reference=${encodeURIComponent(referenceId)}`,
299
+ ),
300
+ cancel_url: getUrl(ctx, ctx.body.cancelUrl),
301
+ line_items: [
302
+ {
303
+ price: ctx.body.annual
304
+ ? plan.annualDiscountPriceId
305
+ : plan.priceId,
306
+ quantity: ctx.body.seats || 1,
307
+ },
308
+ ],
309
+ subscription_data: {
310
+ ...freeTrail,
311
+ },
312
+ mode: "subscription",
313
+ client_reference_id: referenceId,
314
+ ...params?.params,
315
+ metadata: {
316
+ userId: user.id,
317
+ subscriptionId: subscription.id,
318
+ referenceId,
319
+ ...params?.params?.metadata,
314
320
  },
315
- ],
316
- mode: "subscription",
317
- client_reference_id: referenceId,
318
- ...params,
319
- metadata: {
320
- userId: user.id,
321
- subscriptionId: subscription.id,
322
- referenceId,
323
- ...params?.params?.metadata,
324
321
  },
325
- })
322
+ params?.options,
323
+ )
326
324
  .catch(async (e) => {
327
325
  throw ctx.error("BAD_REQUEST", {
328
326
  message: e.message,
@@ -443,12 +441,36 @@ export const stripe = <O extends StripeOptions>(options: O) => {
443
441
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
444
442
  });
445
443
  }
446
- const activeSubscription = await client.subscriptions
444
+ const activeSubscriptions = await client.subscriptions
447
445
  .list({
448
446
  customer: subscription.stripeCustomerId,
449
- status: "active",
450
447
  })
451
- .then((res) => res.data[0]);
448
+ .then((res) =>
449
+ res.data.filter(
450
+ (sub) => sub.status === "active" || sub.status === "trialing",
451
+ ),
452
+ );
453
+ if (!activeSubscriptions.length) {
454
+ /**
455
+ * If the subscription is not found, we need to delete the subscription
456
+ * from the database. This is a rare case and should not happen.
457
+ */
458
+ await ctx.context.adapter.deleteMany({
459
+ model: "subscription",
460
+ where: [
461
+ {
462
+ field: "referenceId",
463
+ value: referenceId,
464
+ },
465
+ ],
466
+ });
467
+ throw ctx.error("BAD_REQUEST", {
468
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
469
+ });
470
+ }
471
+ const activeSubscription = activeSubscriptions.find(
472
+ (sub) => sub.id === subscription.stripeSubscriptionId,
473
+ );
452
474
  if (!activeSubscription) {
453
475
  throw ctx.error("BAD_REQUEST", {
454
476
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
package/src/types.ts CHANGED
@@ -43,12 +43,6 @@ export type Plan = {
43
43
  * Number of days
44
44
  */
45
45
  days: number;
46
- /**
47
- * Only available for new users or users without existing subscription
48
- *
49
- * @default true
50
- */
51
- forNewUsersOnly?: boolean;
52
46
  /**
53
47
  * A function that will be called when the trial
54
48
  * starts.