@better-auth/stripe 1.2.6-beta.6 → 1.2.6

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,17 +1,17 @@
1
1
 
2
- > @better-auth/stripe@1.2.6-beta.6 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.2.6 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: 33.6 kB, chunk size: 33.6 kB, exports: stripe)
8
+ [log] dist/index.cjs (total size: 36.9 kB, chunk size: 36.9 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: 33.3 kB, chunk size: 33.3 kB, exports: stripe)
12
+ [log] dist/index.mjs (total size: 36.6 kB, chunk size: 36.6 kB, exports: stripe)
13
13
 
14
14
  [log] dist/client.mjs (total size: 133 B, chunk size: 133 B, exports: stripeClient)
15
15
 
16
- Σ Total dist size (byte size): 184 kB
16
+ Σ Total dist size (byte size): 199 kB
17
17
  [log]
package/dist/index.cjs CHANGED
@@ -36,7 +36,7 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
36
36
  const priceId = subscription.items.data[0]?.price.id;
37
37
  const plan = await getPlanByPriceId(options, priceId);
38
38
  if (plan) {
39
- const referenceId = checkoutSession?.metadata?.referenceId;
39
+ const referenceId = checkoutSession?.client_reference_id || checkoutSession?.metadata?.referenceId;
40
40
  const subscriptionId = checkoutSession?.metadata?.subscriptionId;
41
41
  const seats = subscription.items.data[0].quantity;
42
42
  if (referenceId && subscriptionId) {
@@ -288,7 +288,9 @@ const STRIPE_ERROR_CODES = {
288
288
  SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
289
289
  ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
290
290
  UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
291
- EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan"
291
+ EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
292
+ SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
293
+ SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: "Subscription is not scheduled for cancellation"
292
294
  };
293
295
  const getUrl = (ctx, url) => {
294
296
  if (url.startsWith("http")) {
@@ -304,6 +306,14 @@ const stripe = (options) => {
304
306
  throw new api.APIError("UNAUTHORIZED");
305
307
  }
306
308
  const referenceId = ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
309
+ if (ctx.body?.referenceId && !options.subscription?.authorizeReference) {
310
+ betterAuth.logger.error(
311
+ `Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`
312
+ );
313
+ throw new api.APIError("BAD_REQUEST", {
314
+ message: "Reference id is not allowed. Read server logs for more details."
315
+ });
316
+ }
307
317
  const isAuthorized = ctx.body?.referenceId ? await options.subscription?.authorizeReference?.({
308
318
  user: session.user,
309
319
  session: session.session,
@@ -762,6 +772,88 @@ const stripe = (options) => {
762
772
  };
763
773
  }
764
774
  ),
775
+ restoreSubscription: plugins.createAuthEndpoint(
776
+ "/subscription/restore",
777
+ {
778
+ method: "POST",
779
+ body: zod.z.object({
780
+ referenceId: zod.z.string().optional(),
781
+ subscriptionId: zod.z.string().optional()
782
+ }),
783
+ use: [api.sessionMiddleware, referenceMiddleware("restore-subscription")]
784
+ },
785
+ async (ctx) => {
786
+ const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
787
+ const subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
788
+ model: "subscription",
789
+ where: [
790
+ {
791
+ field: "id",
792
+ value: ctx.body.subscriptionId
793
+ }
794
+ ]
795
+ }) : await ctx.context.adapter.findOne({
796
+ model: "subscription",
797
+ where: [
798
+ {
799
+ field: "referenceId",
800
+ value: referenceId
801
+ }
802
+ ]
803
+ });
804
+ if (!subscription || !subscription.stripeCustomerId) {
805
+ throw ctx.error("BAD_REQUEST", {
806
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
807
+ });
808
+ }
809
+ if (subscription.status != "active" && subscription.status != "trialing") {
810
+ throw ctx.error("BAD_REQUEST", {
811
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE
812
+ });
813
+ }
814
+ if (!subscription.cancelAtPeriodEnd) {
815
+ throw ctx.error("BAD_REQUEST", {
816
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION
817
+ });
818
+ }
819
+ const activeSubscription = await client.subscriptions.list({
820
+ customer: subscription.stripeCustomerId,
821
+ status: "active"
822
+ }).then((res) => res.data[0]);
823
+ if (!activeSubscription) {
824
+ throw ctx.error("BAD_REQUEST", {
825
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
826
+ });
827
+ }
828
+ try {
829
+ const newSub = await client.subscriptions.update(
830
+ activeSubscription.id,
831
+ {
832
+ cancel_at_period_end: false
833
+ }
834
+ );
835
+ await ctx.context.adapter.update({
836
+ model: "subscription",
837
+ update: {
838
+ cancelAtPeriodEnd: false,
839
+ updatedAt: /* @__PURE__ */ new Date()
840
+ },
841
+ where: [
842
+ {
843
+ field: "id",
844
+ value: subscription.id
845
+ }
846
+ ]
847
+ });
848
+ return ctx.json(newSub);
849
+ } catch (error) {
850
+ ctx.context.logger.error("Error restoring subscription", error);
851
+ throw new api.APIError("BAD_REQUEST", {
852
+ message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER
853
+ });
854
+ }
855
+ }
856
+ ),
765
857
  listActiveSubscriptions: plugins.createAuthEndpoint(
766
858
  "/subscription/list",
767
859
  {
package/dist/index.d.cts CHANGED
@@ -297,7 +297,7 @@ interface StripeOptions {
297
297
  user: User & Record<string, any>;
298
298
  session: Session & Record<string, any>;
299
299
  referenceId: string;
300
- action: "upgrade-subscription" | "list-subscription" | "cancel-subscription";
300
+ action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription";
301
301
  }, request?: Request) => Promise<boolean>;
302
302
  /**
303
303
  * A callback to run after a user has deleted their subscription
@@ -786,6 +786,74 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
786
786
  };
787
787
  path: "/subscription/cancel";
788
788
  };
789
+ readonly restoreSubscription: {
790
+ <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
791
+ body: {
792
+ referenceId?: string | undefined;
793
+ subscriptionId?: string | undefined;
794
+ };
795
+ } & {
796
+ method?: "POST" | undefined;
797
+ } & {
798
+ query?: Record<string, any> | undefined;
799
+ } & {
800
+ params?: Record<string, any>;
801
+ } & {
802
+ request?: Request;
803
+ } & {
804
+ headers?: HeadersInit;
805
+ } & {
806
+ asResponse?: boolean;
807
+ returnHeaders?: boolean;
808
+ use?: better_call.Middleware[];
809
+ path?: string;
810
+ } & {
811
+ asResponse?: AsResponse | undefined;
812
+ returnHeaders?: ReturnHeaders | undefined;
813
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
814
+ headers: Headers;
815
+ response: Stripe.Response<Stripe.Subscription>;
816
+ } : Stripe.Response<Stripe.Subscription>>;
817
+ options: {
818
+ method: "POST";
819
+ body: z.ZodObject<{
820
+ referenceId: z.ZodOptional<z.ZodString>;
821
+ subscriptionId: z.ZodOptional<z.ZodString>;
822
+ }, "strip", z.ZodTypeAny, {
823
+ referenceId?: string | undefined;
824
+ subscriptionId?: string | undefined;
825
+ }, {
826
+ referenceId?: string | undefined;
827
+ subscriptionId?: string | undefined;
828
+ }>;
829
+ use: (((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<{
830
+ session: {
831
+ session: Record<string, any> & {
832
+ id: string;
833
+ createdAt: Date;
834
+ updatedAt: Date;
835
+ userId: string;
836
+ expiresAt: Date;
837
+ token: string;
838
+ ipAddress?: string | null | undefined;
839
+ userAgent?: string | null | undefined;
840
+ };
841
+ user: Record<string, any> & {
842
+ id: string;
843
+ name: string;
844
+ email: string;
845
+ emailVerified: boolean;
846
+ createdAt: Date;
847
+ updatedAt: Date;
848
+ image?: string | null | undefined;
849
+ };
850
+ };
851
+ }>) | ((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<void>))[];
852
+ } & {
853
+ use: any[];
854
+ };
855
+ path: "/subscription/restore";
856
+ };
789
857
  readonly listActiveSubscriptions: {
790
858
  <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({
791
859
  body?: undefined;
package/dist/index.d.mts CHANGED
@@ -297,7 +297,7 @@ interface StripeOptions {
297
297
  user: User & Record<string, any>;
298
298
  session: Session & Record<string, any>;
299
299
  referenceId: string;
300
- action: "upgrade-subscription" | "list-subscription" | "cancel-subscription";
300
+ action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription";
301
301
  }, request?: Request) => Promise<boolean>;
302
302
  /**
303
303
  * A callback to run after a user has deleted their subscription
@@ -786,6 +786,74 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
786
786
  };
787
787
  path: "/subscription/cancel";
788
788
  };
789
+ readonly restoreSubscription: {
790
+ <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
791
+ body: {
792
+ referenceId?: string | undefined;
793
+ subscriptionId?: string | undefined;
794
+ };
795
+ } & {
796
+ method?: "POST" | undefined;
797
+ } & {
798
+ query?: Record<string, any> | undefined;
799
+ } & {
800
+ params?: Record<string, any>;
801
+ } & {
802
+ request?: Request;
803
+ } & {
804
+ headers?: HeadersInit;
805
+ } & {
806
+ asResponse?: boolean;
807
+ returnHeaders?: boolean;
808
+ use?: better_call.Middleware[];
809
+ path?: string;
810
+ } & {
811
+ asResponse?: AsResponse | undefined;
812
+ returnHeaders?: ReturnHeaders | undefined;
813
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
814
+ headers: Headers;
815
+ response: Stripe.Response<Stripe.Subscription>;
816
+ } : Stripe.Response<Stripe.Subscription>>;
817
+ options: {
818
+ method: "POST";
819
+ body: z.ZodObject<{
820
+ referenceId: z.ZodOptional<z.ZodString>;
821
+ subscriptionId: z.ZodOptional<z.ZodString>;
822
+ }, "strip", z.ZodTypeAny, {
823
+ referenceId?: string | undefined;
824
+ subscriptionId?: string | undefined;
825
+ }, {
826
+ referenceId?: string | undefined;
827
+ subscriptionId?: string | undefined;
828
+ }>;
829
+ use: (((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<{
830
+ session: {
831
+ session: Record<string, any> & {
832
+ id: string;
833
+ createdAt: Date;
834
+ updatedAt: Date;
835
+ userId: string;
836
+ expiresAt: Date;
837
+ token: string;
838
+ ipAddress?: string | null | undefined;
839
+ userAgent?: string | null | undefined;
840
+ };
841
+ user: Record<string, any> & {
842
+ id: string;
843
+ name: string;
844
+ email: string;
845
+ emailVerified: boolean;
846
+ createdAt: Date;
847
+ updatedAt: Date;
848
+ image?: string | null | undefined;
849
+ };
850
+ };
851
+ }>) | ((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<void>))[];
852
+ } & {
853
+ use: any[];
854
+ };
855
+ path: "/subscription/restore";
856
+ };
789
857
  readonly listActiveSubscriptions: {
790
858
  <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({
791
859
  body?: undefined;
package/dist/index.d.ts CHANGED
@@ -297,7 +297,7 @@ interface StripeOptions {
297
297
  user: User & Record<string, any>;
298
298
  session: Session & Record<string, any>;
299
299
  referenceId: string;
300
- action: "upgrade-subscription" | "list-subscription" | "cancel-subscription";
300
+ action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription";
301
301
  }, request?: Request) => Promise<boolean>;
302
302
  /**
303
303
  * A callback to run after a user has deleted their subscription
@@ -786,6 +786,74 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
786
786
  };
787
787
  path: "/subscription/cancel";
788
788
  };
789
+ readonly restoreSubscription: {
790
+ <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
791
+ body: {
792
+ referenceId?: string | undefined;
793
+ subscriptionId?: string | undefined;
794
+ };
795
+ } & {
796
+ method?: "POST" | undefined;
797
+ } & {
798
+ query?: Record<string, any> | undefined;
799
+ } & {
800
+ params?: Record<string, any>;
801
+ } & {
802
+ request?: Request;
803
+ } & {
804
+ headers?: HeadersInit;
805
+ } & {
806
+ asResponse?: boolean;
807
+ returnHeaders?: boolean;
808
+ use?: better_call.Middleware[];
809
+ path?: string;
810
+ } & {
811
+ asResponse?: AsResponse | undefined;
812
+ returnHeaders?: ReturnHeaders | undefined;
813
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
814
+ headers: Headers;
815
+ response: Stripe.Response<Stripe.Subscription>;
816
+ } : Stripe.Response<Stripe.Subscription>>;
817
+ options: {
818
+ method: "POST";
819
+ body: z.ZodObject<{
820
+ referenceId: z.ZodOptional<z.ZodString>;
821
+ subscriptionId: z.ZodOptional<z.ZodString>;
822
+ }, "strip", z.ZodTypeAny, {
823
+ referenceId?: string | undefined;
824
+ subscriptionId?: string | undefined;
825
+ }, {
826
+ referenceId?: string | undefined;
827
+ subscriptionId?: string | undefined;
828
+ }>;
829
+ use: (((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<{
830
+ session: {
831
+ session: Record<string, any> & {
832
+ id: string;
833
+ createdAt: Date;
834
+ updatedAt: Date;
835
+ userId: string;
836
+ expiresAt: Date;
837
+ token: string;
838
+ ipAddress?: string | null | undefined;
839
+ userAgent?: string | null | undefined;
840
+ };
841
+ user: Record<string, any> & {
842
+ id: string;
843
+ name: string;
844
+ email: string;
845
+ emailVerified: boolean;
846
+ createdAt: Date;
847
+ updatedAt: Date;
848
+ image?: string | null | undefined;
849
+ };
850
+ };
851
+ }>) | ((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<void>))[];
852
+ } & {
853
+ use: any[];
854
+ };
855
+ path: "/subscription/restore";
856
+ };
789
857
  readonly listActiveSubscriptions: {
790
858
  <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0?: ({
791
859
  body?: undefined;
package/dist/index.mjs CHANGED
@@ -34,7 +34,7 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
34
34
  const priceId = subscription.items.data[0]?.price.id;
35
35
  const plan = await getPlanByPriceId(options, priceId);
36
36
  if (plan) {
37
- const referenceId = checkoutSession?.metadata?.referenceId;
37
+ const referenceId = checkoutSession?.client_reference_id || checkoutSession?.metadata?.referenceId;
38
38
  const subscriptionId = checkoutSession?.metadata?.subscriptionId;
39
39
  const seats = subscription.items.data[0].quantity;
40
40
  if (referenceId && subscriptionId) {
@@ -286,7 +286,9 @@ const STRIPE_ERROR_CODES = {
286
286
  SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
287
287
  ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
288
288
  UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
289
- EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan"
289
+ EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
290
+ SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
291
+ SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: "Subscription is not scheduled for cancellation"
290
292
  };
291
293
  const getUrl = (ctx, url) => {
292
294
  if (url.startsWith("http")) {
@@ -302,6 +304,14 @@ const stripe = (options) => {
302
304
  throw new APIError("UNAUTHORIZED");
303
305
  }
304
306
  const referenceId = ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
307
+ if (ctx.body?.referenceId && !options.subscription?.authorizeReference) {
308
+ logger.error(
309
+ `Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`
310
+ );
311
+ throw new APIError("BAD_REQUEST", {
312
+ message: "Reference id is not allowed. Read server logs for more details."
313
+ });
314
+ }
305
315
  const isAuthorized = ctx.body?.referenceId ? await options.subscription?.authorizeReference?.({
306
316
  user: session.user,
307
317
  session: session.session,
@@ -760,6 +770,88 @@ const stripe = (options) => {
760
770
  };
761
771
  }
762
772
  ),
773
+ restoreSubscription: createAuthEndpoint(
774
+ "/subscription/restore",
775
+ {
776
+ method: "POST",
777
+ body: z.object({
778
+ referenceId: z.string().optional(),
779
+ subscriptionId: z.string().optional()
780
+ }),
781
+ use: [sessionMiddleware, referenceMiddleware("restore-subscription")]
782
+ },
783
+ async (ctx) => {
784
+ const referenceId = ctx.body?.referenceId || ctx.context.session.user.id;
785
+ const subscription = ctx.body.subscriptionId ? await ctx.context.adapter.findOne({
786
+ model: "subscription",
787
+ where: [
788
+ {
789
+ field: "id",
790
+ value: ctx.body.subscriptionId
791
+ }
792
+ ]
793
+ }) : await ctx.context.adapter.findOne({
794
+ model: "subscription",
795
+ where: [
796
+ {
797
+ field: "referenceId",
798
+ value: referenceId
799
+ }
800
+ ]
801
+ });
802
+ if (!subscription || !subscription.stripeCustomerId) {
803
+ throw ctx.error("BAD_REQUEST", {
804
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
805
+ });
806
+ }
807
+ if (subscription.status != "active" && subscription.status != "trialing") {
808
+ throw ctx.error("BAD_REQUEST", {
809
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE
810
+ });
811
+ }
812
+ if (!subscription.cancelAtPeriodEnd) {
813
+ throw ctx.error("BAD_REQUEST", {
814
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION
815
+ });
816
+ }
817
+ const activeSubscription = await client.subscriptions.list({
818
+ customer: subscription.stripeCustomerId,
819
+ status: "active"
820
+ }).then((res) => res.data[0]);
821
+ if (!activeSubscription) {
822
+ throw ctx.error("BAD_REQUEST", {
823
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
824
+ });
825
+ }
826
+ try {
827
+ const newSub = await client.subscriptions.update(
828
+ activeSubscription.id,
829
+ {
830
+ cancel_at_period_end: false
831
+ }
832
+ );
833
+ await ctx.context.adapter.update({
834
+ model: "subscription",
835
+ update: {
836
+ cancelAtPeriodEnd: false,
837
+ updatedAt: /* @__PURE__ */ new Date()
838
+ },
839
+ where: [
840
+ {
841
+ field: "id",
842
+ value: subscription.id
843
+ }
844
+ ]
845
+ });
846
+ return ctx.json(newSub);
847
+ } catch (error) {
848
+ ctx.context.logger.error("Error restoring subscription", error);
849
+ throw new APIError("BAD_REQUEST", {
850
+ message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER
851
+ });
852
+ }
853
+ }
854
+ ),
763
855
  listActiveSubscriptions: createAuthEndpoint(
764
856
  "/subscription/list",
765
857
  {
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.6-beta.6",
4
+ "version": "1.2.6",
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.6-beta.6"
38
+ "better-auth": "^1.2.6"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/better-sqlite3": "^7.6.12",
package/src/hooks.ts CHANGED
@@ -20,7 +20,9 @@ export async function onCheckoutSessionCompleted(
20
20
  const priceId = subscription.items.data[0]?.price.id;
21
21
  const plan = await getPlanByPriceId(options, priceId as string);
22
22
  if (plan) {
23
- const referenceId = checkoutSession?.metadata?.referenceId;
23
+ const referenceId =
24
+ checkoutSession?.client_reference_id ||
25
+ checkoutSession?.metadata?.referenceId;
24
26
  const subscriptionId = checkoutSession?.metadata?.subscriptionId;
25
27
  const seats = subscription.items.data[0].quantity;
26
28
  if (referenceId && subscriptionId) {
package/src/index.ts CHANGED
@@ -35,6 +35,9 @@ const STRIPE_ERROR_CODES = {
35
35
  FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
36
36
  EMAIL_VERIFICATION_REQUIRED:
37
37
  "Email verification is required before you can subscribe to a plan",
38
+ SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
39
+ SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION:
40
+ "Subscription is not scheduled for cancellation",
38
41
  } as const;
39
42
 
40
43
  const getUrl = (ctx: GenericEndpointContext, url: string) => {
@@ -53,7 +56,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
53
56
  action:
54
57
  | "upgrade-subscription"
55
58
  | "list-subscription"
56
- | "cancel-subscription",
59
+ | "cancel-subscription"
60
+ | "restore-subscription",
57
61
  ) =>
58
62
  createAuthMiddleware(async (ctx) => {
59
63
  const session = ctx.context.session;
@@ -62,6 +66,16 @@ export const stripe = <O extends StripeOptions>(options: O) => {
62
66
  }
63
67
  const referenceId =
64
68
  ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
69
+
70
+ if (ctx.body?.referenceId && !options.subscription?.authorizeReference) {
71
+ logger.error(
72
+ `Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`,
73
+ );
74
+ throw new APIError("BAD_REQUEST", {
75
+ message:
76
+ "Reference id is not allowed. Read server logs for more details.",
77
+ });
78
+ }
65
79
  const isAuthorized = ctx.body?.referenceId
66
80
  ? await options.subscription?.authorizeReference?.({
67
81
  user: session.user,
@@ -611,6 +625,102 @@ export const stripe = <O extends StripeOptions>(options: O) => {
611
625
  };
612
626
  },
613
627
  ),
628
+ restoreSubscription: createAuthEndpoint(
629
+ "/subscription/restore",
630
+ {
631
+ method: "POST",
632
+ body: z.object({
633
+ referenceId: z.string().optional(),
634
+ subscriptionId: z.string().optional(),
635
+ }),
636
+ use: [sessionMiddleware, referenceMiddleware("restore-subscription")],
637
+ },
638
+ async (ctx) => {
639
+ const referenceId =
640
+ ctx.body?.referenceId || ctx.context.session.user.id;
641
+
642
+ const subscription = ctx.body.subscriptionId
643
+ ? await ctx.context.adapter.findOne<Subscription>({
644
+ model: "subscription",
645
+ where: [
646
+ {
647
+ field: "id",
648
+ value: ctx.body.subscriptionId,
649
+ },
650
+ ],
651
+ })
652
+ : await ctx.context.adapter.findOne<Subscription>({
653
+ model: "subscription",
654
+ where: [
655
+ {
656
+ field: "referenceId",
657
+ value: referenceId,
658
+ },
659
+ ],
660
+ });
661
+ if (!subscription || !subscription.stripeCustomerId) {
662
+ throw ctx.error("BAD_REQUEST", {
663
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
664
+ });
665
+ }
666
+ if (
667
+ subscription.status != "active" &&
668
+ subscription.status != "trialing"
669
+ ) {
670
+ throw ctx.error("BAD_REQUEST", {
671
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE,
672
+ });
673
+ }
674
+ if (!subscription.cancelAtPeriodEnd) {
675
+ throw ctx.error("BAD_REQUEST", {
676
+ message:
677
+ STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION,
678
+ });
679
+ }
680
+
681
+ const activeSubscription = await client.subscriptions
682
+ .list({
683
+ customer: subscription.stripeCustomerId,
684
+ status: "active",
685
+ })
686
+ .then((res) => res.data[0]);
687
+ if (!activeSubscription) {
688
+ throw ctx.error("BAD_REQUEST", {
689
+ message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
690
+ });
691
+ }
692
+
693
+ try {
694
+ const newSub = await client.subscriptions.update(
695
+ activeSubscription.id,
696
+ {
697
+ cancel_at_period_end: false,
698
+ },
699
+ );
700
+
701
+ await ctx.context.adapter.update({
702
+ model: "subscription",
703
+ update: {
704
+ cancelAtPeriodEnd: false,
705
+ updatedAt: new Date(),
706
+ },
707
+ where: [
708
+ {
709
+ field: "id",
710
+ value: subscription.id,
711
+ },
712
+ ],
713
+ });
714
+
715
+ return ctx.json(newSub);
716
+ } catch (error) {
717
+ ctx.context.logger.error("Error restoring subscription", error);
718
+ throw new APIError("BAD_REQUEST", {
719
+ message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER,
720
+ });
721
+ }
722
+ },
723
+ ),
614
724
  listActiveSubscriptions: createAuthEndpoint(
615
725
  "/subscription/list",
616
726
  {
@@ -32,6 +32,7 @@ describe("stripe", async () => {
32
32
  subscriptions: {
33
33
  retrieve: vi.fn(),
34
34
  list: vi.fn().mockResolvedValue({ data: [] }),
35
+ update: vi.fn(),
35
36
  },
36
37
  webhooks: {
37
38
  constructEvent: vi.fn(),
@@ -243,19 +244,15 @@ describe("stripe", async () => {
243
244
  });
244
245
 
245
246
  it("should handle subscription webhook events", async () => {
246
- const testSubscriptionId = "sub_123456";
247
- const testReferenceId = "user_123";
248
- await ctx.adapter.create({
247
+ const { id: testReferenceId } = await ctx.adapter.create({
249
248
  model: "user",
250
249
  data: {
251
- id: testReferenceId,
252
250
  email: "test@email.com",
253
251
  },
254
252
  });
255
- await ctx.adapter.create({
253
+ const { id: testSubscriptionId } = await ctx.adapter.create({
256
254
  model: "subscription",
257
255
  data: {
258
- id: testSubscriptionId,
259
256
  referenceId: testReferenceId,
260
257
  stripeCustomerId: "cus_mock123",
261
258
  status: "active",
@@ -353,22 +350,19 @@ describe("stripe", async () => {
353
350
  });
354
351
  });
355
352
 
353
+ const { id: userId } = await ctx.adapter.create({
354
+ model: "user",
355
+ data: {
356
+ email: "delete-test@email.com",
357
+ },
358
+ });
359
+
356
360
  it("should handle subscription deletion webhook", async () => {
357
- const userId = "test_user";
358
361
  const subId = "test_sub_delete";
359
362
 
360
- await ctx.adapter.create({
361
- model: "user",
362
- data: {
363
- id: userId,
364
- email: "delete-test@email.com",
365
- },
366
- });
367
-
368
363
  await ctx.adapter.create({
369
364
  model: "subscription",
370
365
  data: {
371
- id: subId,
372
366
  referenceId: userId,
373
367
  stripeCustomerId: "cus_delete_test",
374
368
  status: "active",
@@ -501,7 +495,6 @@ describe("stripe", async () => {
501
495
  };
502
496
 
503
497
  const mockSubscription = {
504
- id: "sub_123",
505
498
  status: "active",
506
499
  items: {
507
500
  data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
@@ -532,11 +525,10 @@ describe("stripe", async () => {
532
525
  plugins: [stripe(eventTestOptions)],
533
526
  });
534
527
 
535
- await ctx.adapter.create({
528
+ const { id: testSubscriptionId } = await ctx.adapter.create({
536
529
  model: "subscription",
537
530
  data: {
538
- id: "sub_123",
539
- referenceId: "user_123",
531
+ referenceId: userId,
540
532
  stripeCustomerId: "cus_123",
541
533
  stripeSubscriptionId: "sub_123",
542
534
  status: "incomplete",
@@ -570,7 +562,7 @@ describe("stripe", async () => {
570
562
  type: "customer.subscription.updated",
571
563
  data: {
572
564
  object: {
573
- id: "sub_123",
565
+ id: testSubscriptionId,
574
566
  customer: "cus_123",
575
567
  status: "active",
576
568
  items: {
@@ -608,7 +600,7 @@ describe("stripe", async () => {
608
600
  type: "customer.subscription.updated",
609
601
  data: {
610
602
  object: {
611
- id: "sub_123",
603
+ id: testSubscriptionId,
612
604
  customer: "cus_123",
613
605
  status: "active",
614
606
  cancel_at_period_end: true,
@@ -644,7 +636,7 @@ describe("stripe", async () => {
644
636
  type: "customer.subscription.updated",
645
637
  data: {
646
638
  object: {
647
- id: "sub_123",
639
+ id: testSubscriptionId,
648
640
  customer: "cus_123",
649
641
  status: "active",
650
642
  cancel_at_period_end: true,
@@ -679,12 +671,12 @@ describe("stripe", async () => {
679
671
  type: "customer.subscription.deleted",
680
672
  data: {
681
673
  object: {
682
- id: "sub_123",
674
+ id: testSubscriptionId,
683
675
  customer: "cus_123",
684
676
  status: "canceled",
685
677
  metadata: {
686
- referenceId: "user_123",
687
- subscriptionId: "sub_123",
678
+ referenceId: userId,
679
+ subscriptionId: testSubscriptionId,
688
680
  },
689
681
  },
690
682
  },
package/src/types.ts CHANGED
@@ -269,7 +269,8 @@ export interface StripeOptions {
269
269
  action:
270
270
  | "upgrade-subscription"
271
271
  | "list-subscription"
272
- | "cancel-subscription";
272
+ | "cancel-subscription"
273
+ | "restore-subscription";
273
274
  },
274
275
  request?: Request,
275
276
  ) => Promise<boolean>;