@better-auth/stripe 1.2.3 → 1.2.4-beta.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/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
+ import { logger } from 'better-auth';
1
2
  import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
2
3
  import { z } from 'zod';
3
- import { getSessionFromCtx, sessionMiddleware, originCheck, APIError } from 'better-auth/api';
4
+ import { originCheck, getSessionFromCtx, sessionMiddleware, APIError } from 'better-auth/api';
4
5
  import { generateRandomString } from 'better-auth/crypto';
5
- import { logger } from 'better-auth';
6
6
 
7
7
  async function getPlans(options) {
8
8
  return typeof options?.subscription?.plans === "function" ? await options.subscription?.plans() : options.subscription?.plans;
@@ -93,11 +93,11 @@ async function onSubscriptionUpdated(ctx, options, event) {
93
93
  const subscriptionUpdated = event.data.object;
94
94
  const priceId = subscriptionUpdated.items.data[0].price.id;
95
95
  const plan = await getPlanByPriceId(options, priceId);
96
- const referenceId = subscriptionUpdated.metadata?.referenceId;
96
+ const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
97
97
  const customerId = subscriptionUpdated.customer?.toString();
98
98
  let subscription = await ctx.context.adapter.findOne({
99
99
  model: "subscription",
100
- where: referenceId ? [{ field: "referenceId", value: referenceId }] : [{ field: "stripeSubscriptionId", value: subscriptionUpdated.id }]
100
+ where: subscriptionId ? [{ field: "id", value: subscriptionId }] : [{ field: "stripeSubscriptionId", value: subscriptionUpdated.id }]
101
101
  });
102
102
  if (!subscription) {
103
103
  const subs = await ctx.context.adapter.findMany({
@@ -311,25 +311,64 @@ const stripe = (options) => {
311
311
  {
312
312
  method: "POST",
313
313
  body: z.object({
314
+ /**
315
+ * The name of the plan to subscribe
316
+ */
314
317
  plan: z.string({
315
318
  description: "The name of the plan to upgrade to"
316
319
  }),
320
+ /**
321
+ * If annual plan should be applied.
322
+ */
317
323
  annual: z.boolean({
318
324
  description: "Whether to upgrade to an annual plan"
319
325
  }).optional(),
320
- referenceId: z.string().optional(),
326
+ /**
327
+ * Reference id of the subscription to upgrade
328
+ * This is used to identify the subscription to upgrade
329
+ * If not provided, the user's id will be used
330
+ */
331
+ referenceId: z.string({
332
+ description: "Reference id of the subscription to upgrade"
333
+ }).optional(),
334
+ /**
335
+ * This is to allow a specific subscription to be upgrade.
336
+ * If subscription id is provided, and subscription isn't found,
337
+ * it'll throw an error.
338
+ */
339
+ subscriptionId: z.string({
340
+ description: "The id of the subscription to upgrade"
341
+ }).optional(),
342
+ /**
343
+ * Any additional data you want to store in your database
344
+ * subscriptions
345
+ */
321
346
  metadata: z.record(z.string(), z.any()).optional(),
347
+ /**
348
+ * If a subscription
349
+ */
322
350
  seats: z.number({
323
351
  description: "Number of seats to upgrade to (if applicable)"
324
352
  }).optional(),
325
- uiMode: z.enum(["embedded", "hosted"]).default("hosted"),
353
+ /**
354
+ * Success url to redirect back after successful subscription
355
+ */
326
356
  successUrl: z.string({
327
357
  description: "callback url to redirect back after successful subscription"
328
358
  }).default("/"),
359
+ /**
360
+ * Cancel URL
361
+ */
329
362
  cancelUrl: z.string({
330
363
  description: "callback url to redirect back after successful subscription"
331
364
  }).default("/"),
365
+ /**
366
+ * Return URL
367
+ */
332
368
  returnUrl: z.string().optional(),
369
+ /**
370
+ * Disable Redirect
371
+ */
333
372
  disableRedirect: z.boolean().default(false)
334
373
  }),
335
374
  use: [
@@ -354,7 +393,16 @@ const stripe = (options) => {
354
393
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND
355
394
  });
356
395
  }
357
- let customerId = user.stripeCustomerId;
396
+ const subscriptionToUpdate = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
397
+ model: "subscription",
398
+ where: [{ field: "id", value: ctx.body.subscriptionId }]
399
+ }) : null;
400
+ if (ctx.body.subscriptionId && !subscriptionToUpdate) {
401
+ throw new APIError("BAD_REQUEST", {
402
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
403
+ });
404
+ }
405
+ let customerId = subscriptionToUpdate?.stripeCustomerId || user.stripeCustomerId;
358
406
  if (!customerId) {
359
407
  try {
360
408
  const stripeCustomer = await client.customers.create(
@@ -394,7 +442,7 @@ const stripe = (options) => {
394
442
  customer: customerId,
395
443
  status: "active"
396
444
  }).then((res) => res.data[0]).catch((e) => null) : null;
397
- const subscriptions = await ctx.context.adapter.findMany({
445
+ const subscriptions = subscriptionToUpdate ? [subscriptionToUpdate] : await ctx.context.adapter.findMany({
398
446
  model: "subscription",
399
447
  where: [
400
448
  {
@@ -484,7 +532,7 @@ const stripe = (options) => {
484
532
  ctx,
485
533
  `${ctx.context.baseURL}/subscription/success?callbackURL=${encodeURIComponent(
486
534
  ctx.body.successUrl
487
- )}&reference=${encodeURIComponent(referenceId)}`
535
+ )}&subscriptionId=${encodeURIComponent(subscription.id)}`
488
536
  ),
489
537
  cancel_url: getUrl(ctx, ctx.body.cancelUrl),
490
538
  line_items: [
@@ -523,10 +571,11 @@ const stripe = (options) => {
523
571
  "/subscription/cancel/callback",
524
572
  {
525
573
  method: "GET",
526
- query: z.record(z.string(), z.any()).optional()
574
+ query: z.record(z.string(), z.any()).optional(),
575
+ use: [originCheck((ctx) => ctx.query.callbackURL)]
527
576
  },
528
577
  async (ctx) => {
529
- if (!ctx.query || !ctx.query.callbackURL || !ctx.query.reference) {
578
+ if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) {
530
579
  throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
531
580
  }
532
581
  const session = await getSessionFromCtx(
@@ -536,15 +585,15 @@ const stripe = (options) => {
536
585
  throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
537
586
  }
538
587
  const { user } = session;
539
- const { callbackURL, reference } = ctx.query;
588
+ const { callbackURL, subscriptionId } = ctx.query;
540
589
  if (user?.stripeCustomerId) {
541
590
  try {
542
591
  const subscription = await ctx.context.adapter.findOne({
543
592
  model: "subscription",
544
593
  where: [
545
594
  {
546
- field: "referenceId",
547
- value: reference
595
+ field: "id",
596
+ value: subscriptionId
548
597
  }
549
598
  ]
550
599
  });
@@ -567,8 +616,8 @@ const stripe = (options) => {
567
616
  },
568
617
  where: [
569
618
  {
570
- field: "referenceId",
571
- value: reference
619
+ field: "id",
620
+ value: subscription.id
572
621
  }
573
622
  ]
574
623
  });
@@ -595,6 +644,7 @@ const stripe = (options) => {
595
644
  method: "POST",
596
645
  body: z.object({
597
646
  referenceId: z.string().optional(),
647
+ subscriptionId: z.string().optional(),
598
648
  returnUrl: z.string()
599
649
  }),
600
650
  use: [
@@ -605,15 +655,22 @@ const stripe = (options) => {
605
655
  },
606
656
  async (ctx) => {
607
657
  const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
608
- const subscription = await ctx.context.adapter.findOne({
658
+ const subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
609
659
  model: "subscription",
610
660
  where: [
611
661
  {
612
- field: "referenceId",
613
- value: referenceId
662
+ field: "id",
663
+ value: ctx.body.subscriptionId
614
664
  }
615
665
  ]
616
- });
666
+ }) : await ctx.context.adapter.findMany({
667
+ model: "subscription",
668
+ where: [{ field: "referenceId", value: referenceId }]
669
+ }).then(
670
+ (subs) => subs.find(
671
+ (sub) => sub.status === "active" || sub.status === "trialing"
672
+ )
673
+ );
617
674
  if (!subscription || !subscription.stripeCustomerId) {
618
675
  throw ctx.error("BAD_REQUEST", {
619
676
  message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
@@ -654,7 +711,7 @@ const stripe = (options) => {
654
711
  ctx,
655
712
  `${ctx.context.baseURL}/subscription/cancel/callback?callbackURL=${encodeURIComponent(
656
713
  ctx.body?.returnUrl || "/"
657
- )}&reference=${encodeURIComponent(referenceId)}`
714
+ )}&subscriptionId=${encodeURIComponent(subscription.id)}`
658
715
  ),
659
716
  flow_data: {
660
717
  type: "subscription_cancel",
@@ -736,10 +793,11 @@ const stripe = (options) => {
736
793
  "/subscription/success",
737
794
  {
738
795
  method: "GET",
739
- query: z.record(z.string(), z.any()).optional()
796
+ query: z.record(z.string(), z.any()).optional(),
797
+ use: [originCheck((ctx) => ctx.query.callbackURL)]
740
798
  },
741
799
  async (ctx) => {
742
- if (!ctx.query || !ctx.query.callbackURL || !ctx.query.reference) {
800
+ if (!ctx.query || !ctx.query.callbackURL || !ctx.query.subscriptionId) {
743
801
  throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
744
802
  }
745
803
  const session = await getSessionFromCtx(
@@ -749,38 +807,24 @@ const stripe = (options) => {
749
807
  throw ctx.redirect(getUrl(ctx, ctx.query?.callbackURL || "/"));
750
808
  }
751
809
  const { user } = session;
752
- const { callbackURL, reference } = ctx.query;
753
- const subscriptions = await ctx.context.adapter.findMany({
810
+ const { callbackURL, subscriptionId } = ctx.query;
811
+ const subscription = await ctx.context.adapter.findOne({
754
812
  model: "subscription",
755
813
  where: [
756
814
  {
757
- field: "referenceId",
758
- value: reference
815
+ field: "id",
816
+ value: subscriptionId
759
817
  }
760
818
  ]
761
819
  });
762
- const activeSubscription = subscriptions.find(
763
- (sub) => sub.status === "active" || sub.status === "trialing"
764
- );
765
- if (activeSubscription) {
820
+ if (subscription?.status === "active" || subscription?.status === "trialing") {
766
821
  return ctx.redirect(getUrl(ctx, callbackURL));
767
822
  }
768
- if (user?.stripeCustomerId) {
823
+ const customerId = subscription?.stripeCustomerId || user.stripeCustomerId;
824
+ if (customerId) {
769
825
  try {
770
- const subscription = await ctx.context.adapter.findOne({
771
- model: "subscription",
772
- where: [
773
- {
774
- field: "referenceId",
775
- value: reference
776
- }
777
- ]
778
- });
779
- if (!subscription || subscription.status === "active") {
780
- throw ctx.redirect(getUrl(ctx, callbackURL));
781
- }
782
826
  const stripeSubscription = await client.subscriptions.list({
783
- customer: user.stripeCustomerId,
827
+ customer: customerId,
784
828
  status: "active"
785
829
  }).then((res) => res.data[0]);
786
830
  if (stripeSubscription) {
@@ -788,21 +832,33 @@ const stripe = (options) => {
788
832
  options,
789
833
  stripeSubscription.items.data[0]?.plan.id
790
834
  );
791
- if (plan && subscriptions.length > 0) {
835
+ if (plan && subscription) {
792
836
  await ctx.context.adapter.update({
793
837
  model: "subscription",
794
838
  update: {
795
839
  status: stripeSubscription.status,
796
840
  seats: stripeSubscription.items.data[0]?.quantity || 1,
797
841
  plan: plan.name.toLowerCase(),
798
- periodEnd: stripeSubscription.current_period_end,
799
- periodStart: stripeSubscription.current_period_start,
800
- stripeSubscriptionId: stripeSubscription.id
842
+ periodEnd: new Date(
843
+ stripeSubscription.current_period_end * 1e3
844
+ ),
845
+ periodStart: new Date(
846
+ stripeSubscription.current_period_start * 1e3
847
+ ),
848
+ stripeSubscriptionId: stripeSubscription.id,
849
+ ...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
850
+ trialStart: new Date(
851
+ stripeSubscription.trial_start * 1e3
852
+ ),
853
+ trialEnd: new Date(
854
+ stripeSubscription.trial_end * 1e3
855
+ )
856
+ } : {}
801
857
  },
802
858
  where: [
803
859
  {
804
- field: "referenceId",
805
- value: reference
860
+ field: "id",
861
+ value: subscription.id
806
862
  }
807
863
  ]
808
864
  });
@@ -845,7 +901,11 @@ const stripe = (options) => {
845
901
  message: "Stripe webhook secret not found"
846
902
  });
847
903
  }
848
- event = client.webhooks.constructEvent(buf, sig, webhookSecret);
904
+ event = await client.webhooks.constructEventAsync(
905
+ buf,
906
+ sig,
907
+ webhookSecret
908
+ );
849
909
  } catch (err) {
850
910
  ctx.context.logger.error(`${err.message}`);
851
911
  throw new APIError("BAD_REQUEST", {
@@ -898,18 +958,29 @@ const stripe = (options) => {
898
958
  userId: user.id
899
959
  }
900
960
  });
901
- await ctx2.context.adapter.update({
902
- model: "user",
903
- update: {
904
- stripeCustomerId: stripeCustomer.id
905
- },
906
- where: [
907
- {
908
- field: "id",
909
- value: user.id
910
- }
911
- ]
912
- });
961
+ const customer = await ctx2.context.adapter.update(
962
+ {
963
+ model: "user",
964
+ update: {
965
+ stripeCustomerId: stripeCustomer.id
966
+ },
967
+ where: [
968
+ {
969
+ field: "id",
970
+ value: user.id
971
+ }
972
+ ]
973
+ }
974
+ );
975
+ if (!customer) {
976
+ logger.error("#BETTER_AUTH: Failed to create customer");
977
+ } else {
978
+ await options.onCustomerCreate?.({
979
+ customer,
980
+ stripeCustomer,
981
+ user
982
+ });
983
+ }
913
984
  }
914
985
  }
915
986
  }
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.3",
4
+ "version": "1.2.4-beta.10",
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.3"
38
+ "better-auth": "^1.2.4-beta.10"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/better-sqlite3": "^7.6.12",
package/src/client.ts CHANGED
@@ -15,7 +15,7 @@ export const stripeClient = <
15
15
  O["subscription"] extends true
16
16
  ? {
17
17
  stripeClient: any;
18
- stripeWebhookSecret: "";
18
+ stripeWebhookSecret: string;
19
19
  subscription: {
20
20
  enabled: true;
21
21
  plans: [];
@@ -23,7 +23,7 @@ export const stripeClient = <
23
23
  }
24
24
  : {
25
25
  stripeClient: any;
26
- stripeWebhookSecret: "";
26
+ stripeWebhookSecret: string;
27
27
  }
28
28
  >
29
29
  >,
package/src/hooks.ts CHANGED
@@ -95,12 +95,12 @@ export async function onSubscriptionUpdated(
95
95
  const priceId = subscriptionUpdated.items.data[0].price.id;
96
96
  const plan = await getPlanByPriceId(options, priceId);
97
97
 
98
- const referenceId = subscriptionUpdated.metadata?.referenceId;
98
+ const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
99
99
  const customerId = subscriptionUpdated.customer?.toString();
100
100
  let subscription = await ctx.context.adapter.findOne<Subscription>({
101
101
  model: "subscription",
102
- where: referenceId
103
- ? [{ field: "referenceId", value: referenceId }]
102
+ where: subscriptionId
103
+ ? [{ field: "id", value: subscriptionId }]
104
104
  : [{ field: "stripeSubscriptionId", value: subscriptionUpdated.id }],
105
105
  });
106
106
  if (!subscription) {