@better-auth/stripe 1.3.5-beta.3 → 1.3.5-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,17 +1,17 @@
1
1
 
2
- > @better-auth/stripe@1.3.5-beta.3 build /home/runner/work/better-auth/better-auth/packages/stripe
2
+ > @better-auth/stripe@1.3.5-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: 41.5 kB, chunk size: 41.5 kB, exports: stripe)
8
+ [log] dist/index.cjs (total size: 43.3 kB, chunk size: 43.3 kB, exports: stripe)
9
9
 
10
10
  [log] dist/client.cjs (total size: 224 B, chunk size: 224 B, exports: stripeClient)
11
11
 
12
- [log] dist/index.mjs (total size: 40.7 kB, chunk size: 40.7 kB, exports: stripe)
12
+ [log] dist/index.mjs (total size: 42.5 kB, chunk size: 42.5 kB, exports: stripe)
13
13
 
14
14
  [log] dist/client.mjs (total size: 197 B, chunk size: 197 B, exports: stripeClient)
15
15
 
16
- Σ Total dist size (byte size): 204 kB
16
+ Σ Total dist size (byte size): 216 kB
17
17
  [log]
package/dist/index.cjs CHANGED
@@ -1123,6 +1123,65 @@ const stripe = (options) => {
1123
1123
  }
1124
1124
  throw ctx.redirect(getUrl(ctx, callbackURL));
1125
1125
  }
1126
+ ),
1127
+ createBillingPortal: plugins.createAuthEndpoint(
1128
+ "/subscription/billing-portal",
1129
+ {
1130
+ method: "POST",
1131
+ body: z__namespace.object({
1132
+ referenceId: z__namespace.string().optional(),
1133
+ returnUrl: z__namespace.string().default("/")
1134
+ }),
1135
+ use: [
1136
+ api.sessionMiddleware,
1137
+ api.originCheck((ctx) => ctx.body.returnUrl),
1138
+ referenceMiddleware("billing-portal")
1139
+ ]
1140
+ },
1141
+ async (ctx) => {
1142
+ const { user } = ctx.context.session;
1143
+ const referenceId = ctx.body.referenceId || user.id;
1144
+ let customerId = user.stripeCustomerId;
1145
+ if (!customerId) {
1146
+ const subscription = await ctx.context.adapter.findMany({
1147
+ model: "subscription",
1148
+ where: [
1149
+ {
1150
+ field: "referenceId",
1151
+ value: referenceId
1152
+ }
1153
+ ]
1154
+ }).then(
1155
+ (subs) => subs.find(
1156
+ (sub) => sub.status === "active" || sub.status === "trialing"
1157
+ )
1158
+ );
1159
+ customerId = subscription?.stripeCustomerId;
1160
+ }
1161
+ if (!customerId) {
1162
+ throw new api.APIError("BAD_REQUEST", {
1163
+ message: "No Stripe customer found for this user"
1164
+ });
1165
+ }
1166
+ try {
1167
+ const { url } = await client.billingPortal.sessions.create({
1168
+ customer: customerId,
1169
+ return_url: getUrl(ctx, ctx.body.returnUrl)
1170
+ });
1171
+ return ctx.json({
1172
+ url,
1173
+ redirect: true
1174
+ });
1175
+ } catch (error) {
1176
+ ctx.context.logger.error(
1177
+ "Error creating billing portal session",
1178
+ error
1179
+ );
1180
+ throw new api.APIError("BAD_REQUEST", {
1181
+ message: error.message
1182
+ });
1183
+ }
1184
+ }
1126
1185
  )
1127
1186
  };
1128
1187
  return {
package/dist/index.d.cts CHANGED
@@ -296,7 +296,7 @@ interface StripeOptions {
296
296
  user: User & Record<string, any>;
297
297
  session: Session & Record<string, any>;
298
298
  referenceId: string;
299
- action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription";
299
+ action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription" | "billing-portal";
300
300
  }, ctx: GenericEndpointContext) => Promise<boolean>;
301
301
  /**
302
302
  * A callback to run after a user has deleted their subscription
@@ -957,6 +957,74 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
957
957
  };
958
958
  path: "/subscription/success";
959
959
  };
960
+ readonly createBillingPortal: {
961
+ <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
962
+ body: {
963
+ referenceId?: string | undefined;
964
+ returnUrl?: string | undefined;
965
+ };
966
+ } & {
967
+ method?: "POST" | undefined;
968
+ } & {
969
+ query?: Record<string, any> | undefined;
970
+ } & {
971
+ params?: Record<string, any>;
972
+ } & {
973
+ request?: Request;
974
+ } & {
975
+ headers?: HeadersInit;
976
+ } & {
977
+ asResponse?: boolean;
978
+ returnHeaders?: boolean;
979
+ use?: better_call.Middleware[];
980
+ path?: string;
981
+ } & {
982
+ asResponse?: AsResponse | undefined;
983
+ returnHeaders?: ReturnHeaders | undefined;
984
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
985
+ headers: Headers;
986
+ response: {
987
+ url: string;
988
+ redirect: boolean;
989
+ };
990
+ } : {
991
+ url: string;
992
+ redirect: boolean;
993
+ }>;
994
+ options: {
995
+ method: "POST";
996
+ body: z.ZodObject<{
997
+ referenceId: z.ZodOptional<z.ZodString>;
998
+ returnUrl: z.ZodDefault<z.ZodString>;
999
+ }, z.core.$strip>;
1000
+ use: (((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<{
1001
+ session: {
1002
+ session: Record<string, any> & {
1003
+ id: string;
1004
+ userId: string;
1005
+ expiresAt: Date;
1006
+ createdAt: Date;
1007
+ updatedAt: Date;
1008
+ token: string;
1009
+ ipAddress?: string | null | undefined;
1010
+ userAgent?: string | null | undefined;
1011
+ };
1012
+ user: Record<string, any> & {
1013
+ id: string;
1014
+ email: string;
1015
+ emailVerified: boolean;
1016
+ name: string;
1017
+ createdAt: Date;
1018
+ updatedAt: Date;
1019
+ image?: string | null | undefined;
1020
+ };
1021
+ };
1022
+ }>) | ((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<void>))[];
1023
+ } & {
1024
+ use: any[];
1025
+ };
1026
+ path: "/subscription/billing-portal";
1027
+ };
960
1028
  } : {});
961
1029
  init(ctx: better_auth.AuthContext): {
962
1030
  options: {
package/dist/index.d.mts CHANGED
@@ -296,7 +296,7 @@ interface StripeOptions {
296
296
  user: User & Record<string, any>;
297
297
  session: Session & Record<string, any>;
298
298
  referenceId: string;
299
- action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription";
299
+ action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription" | "billing-portal";
300
300
  }, ctx: GenericEndpointContext) => Promise<boolean>;
301
301
  /**
302
302
  * A callback to run after a user has deleted their subscription
@@ -957,6 +957,74 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
957
957
  };
958
958
  path: "/subscription/success";
959
959
  };
960
+ readonly createBillingPortal: {
961
+ <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
962
+ body: {
963
+ referenceId?: string | undefined;
964
+ returnUrl?: string | undefined;
965
+ };
966
+ } & {
967
+ method?: "POST" | undefined;
968
+ } & {
969
+ query?: Record<string, any> | undefined;
970
+ } & {
971
+ params?: Record<string, any>;
972
+ } & {
973
+ request?: Request;
974
+ } & {
975
+ headers?: HeadersInit;
976
+ } & {
977
+ asResponse?: boolean;
978
+ returnHeaders?: boolean;
979
+ use?: better_call.Middleware[];
980
+ path?: string;
981
+ } & {
982
+ asResponse?: AsResponse | undefined;
983
+ returnHeaders?: ReturnHeaders | undefined;
984
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
985
+ headers: Headers;
986
+ response: {
987
+ url: string;
988
+ redirect: boolean;
989
+ };
990
+ } : {
991
+ url: string;
992
+ redirect: boolean;
993
+ }>;
994
+ options: {
995
+ method: "POST";
996
+ body: z.ZodObject<{
997
+ referenceId: z.ZodOptional<z.ZodString>;
998
+ returnUrl: z.ZodDefault<z.ZodString>;
999
+ }, z.core.$strip>;
1000
+ use: (((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<{
1001
+ session: {
1002
+ session: Record<string, any> & {
1003
+ id: string;
1004
+ userId: string;
1005
+ expiresAt: Date;
1006
+ createdAt: Date;
1007
+ updatedAt: Date;
1008
+ token: string;
1009
+ ipAddress?: string | null | undefined;
1010
+ userAgent?: string | null | undefined;
1011
+ };
1012
+ user: Record<string, any> & {
1013
+ id: string;
1014
+ email: string;
1015
+ emailVerified: boolean;
1016
+ name: string;
1017
+ createdAt: Date;
1018
+ updatedAt: Date;
1019
+ image?: string | null | undefined;
1020
+ };
1021
+ };
1022
+ }>) | ((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<void>))[];
1023
+ } & {
1024
+ use: any[];
1025
+ };
1026
+ path: "/subscription/billing-portal";
1027
+ };
960
1028
  } : {});
961
1029
  init(ctx: better_auth.AuthContext): {
962
1030
  options: {
package/dist/index.d.ts CHANGED
@@ -296,7 +296,7 @@ interface StripeOptions {
296
296
  user: User & Record<string, any>;
297
297
  session: Session & Record<string, any>;
298
298
  referenceId: string;
299
- action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription";
299
+ action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription" | "billing-portal";
300
300
  }, ctx: GenericEndpointContext) => Promise<boolean>;
301
301
  /**
302
302
  * A callback to run after a user has deleted their subscription
@@ -957,6 +957,74 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
957
957
  };
958
958
  path: "/subscription/success";
959
959
  };
960
+ readonly createBillingPortal: {
961
+ <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
962
+ body: {
963
+ referenceId?: string | undefined;
964
+ returnUrl?: string | undefined;
965
+ };
966
+ } & {
967
+ method?: "POST" | undefined;
968
+ } & {
969
+ query?: Record<string, any> | undefined;
970
+ } & {
971
+ params?: Record<string, any>;
972
+ } & {
973
+ request?: Request;
974
+ } & {
975
+ headers?: HeadersInit;
976
+ } & {
977
+ asResponse?: boolean;
978
+ returnHeaders?: boolean;
979
+ use?: better_call.Middleware[];
980
+ path?: string;
981
+ } & {
982
+ asResponse?: AsResponse | undefined;
983
+ returnHeaders?: ReturnHeaders | undefined;
984
+ }): Promise<[AsResponse] extends [true] ? Response : [ReturnHeaders] extends [true] ? {
985
+ headers: Headers;
986
+ response: {
987
+ url: string;
988
+ redirect: boolean;
989
+ };
990
+ } : {
991
+ url: string;
992
+ redirect: boolean;
993
+ }>;
994
+ options: {
995
+ method: "POST";
996
+ body: z.ZodObject<{
997
+ referenceId: z.ZodOptional<z.ZodString>;
998
+ returnUrl: z.ZodDefault<z.ZodString>;
999
+ }, z.core.$strip>;
1000
+ use: (((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<{
1001
+ session: {
1002
+ session: Record<string, any> & {
1003
+ id: string;
1004
+ userId: string;
1005
+ expiresAt: Date;
1006
+ createdAt: Date;
1007
+ updatedAt: Date;
1008
+ token: string;
1009
+ ipAddress?: string | null | undefined;
1010
+ userAgent?: string | null | undefined;
1011
+ };
1012
+ user: Record<string, any> & {
1013
+ id: string;
1014
+ email: string;
1015
+ emailVerified: boolean;
1016
+ name: string;
1017
+ createdAt: Date;
1018
+ updatedAt: Date;
1019
+ image?: string | null | undefined;
1020
+ };
1021
+ };
1022
+ }>) | ((inputContext: better_call.MiddlewareInputContext<better_call.MiddlewareOptions>) => Promise<void>))[];
1023
+ } & {
1024
+ use: any[];
1025
+ };
1026
+ path: "/subscription/billing-portal";
1027
+ };
960
1028
  } : {});
961
1029
  init(ctx: better_auth.AuthContext): {
962
1030
  options: {
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { logger } from 'better-auth';
2
2
  import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
3
3
  import * as z from 'zod/v4';
4
- import { originCheck, getSessionFromCtx, sessionMiddleware, APIError } from 'better-auth/api';
4
+ import { sessionMiddleware, originCheck, APIError, getSessionFromCtx } from 'better-auth/api';
5
5
  import { mergeSchema } from 'better-auth/db';
6
6
 
7
7
  async function getPlans(options) {
@@ -1107,6 +1107,65 @@ const stripe = (options) => {
1107
1107
  }
1108
1108
  throw ctx.redirect(getUrl(ctx, callbackURL));
1109
1109
  }
1110
+ ),
1111
+ createBillingPortal: createAuthEndpoint(
1112
+ "/subscription/billing-portal",
1113
+ {
1114
+ method: "POST",
1115
+ body: z.object({
1116
+ referenceId: z.string().optional(),
1117
+ returnUrl: z.string().default("/")
1118
+ }),
1119
+ use: [
1120
+ sessionMiddleware,
1121
+ originCheck((ctx) => ctx.body.returnUrl),
1122
+ referenceMiddleware("billing-portal")
1123
+ ]
1124
+ },
1125
+ async (ctx) => {
1126
+ const { user } = ctx.context.session;
1127
+ const referenceId = ctx.body.referenceId || user.id;
1128
+ let customerId = user.stripeCustomerId;
1129
+ if (!customerId) {
1130
+ const subscription = await ctx.context.adapter.findMany({
1131
+ model: "subscription",
1132
+ where: [
1133
+ {
1134
+ field: "referenceId",
1135
+ value: referenceId
1136
+ }
1137
+ ]
1138
+ }).then(
1139
+ (subs) => subs.find(
1140
+ (sub) => sub.status === "active" || sub.status === "trialing"
1141
+ )
1142
+ );
1143
+ customerId = subscription?.stripeCustomerId;
1144
+ }
1145
+ if (!customerId) {
1146
+ throw new APIError("BAD_REQUEST", {
1147
+ message: "No Stripe customer found for this user"
1148
+ });
1149
+ }
1150
+ try {
1151
+ const { url } = await client.billingPortal.sessions.create({
1152
+ customer: customerId,
1153
+ return_url: getUrl(ctx, ctx.body.returnUrl)
1154
+ });
1155
+ return ctx.json({
1156
+ url,
1157
+ redirect: true
1158
+ });
1159
+ } catch (error) {
1160
+ ctx.context.logger.error(
1161
+ "Error creating billing portal session",
1162
+ error
1163
+ );
1164
+ throw new APIError("BAD_REQUEST", {
1165
+ message: error.message
1166
+ });
1167
+ }
1168
+ }
1110
1169
  )
1111
1170
  };
1112
1171
  return {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@better-auth/stripe",
3
3
  "author": "Bereket Engida",
4
- "version": "1.3.5-beta.3",
4
+ "version": "1.3.5-beta.5",
5
5
  "main": "dist/index.cjs",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -37,7 +37,7 @@
37
37
  }
38
38
  },
39
39
  "dependencies": {
40
- "better-auth": "^1.3.5-beta.3"
40
+ "better-auth": "^1.3.5-beta.5"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "zod": "^3.25.0 || ^4.0.0"
package/src/index.ts CHANGED
@@ -69,7 +69,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
69
69
  | "upgrade-subscription"
70
70
  | "list-subscription"
71
71
  | "cancel-subscription"
72
- | "restore-subscription",
72
+ | "restore-subscription"
73
+ | "billing-portal",
73
74
  ) =>
74
75
  createAuthMiddleware(async (ctx) => {
75
76
  const session = ctx.context.session;
@@ -1033,6 +1034,73 @@ export const stripe = <O extends StripeOptions>(options: O) => {
1033
1034
  throw ctx.redirect(getUrl(ctx, callbackURL));
1034
1035
  },
1035
1036
  ),
1037
+ createBillingPortal: createAuthEndpoint(
1038
+ "/subscription/billing-portal",
1039
+ {
1040
+ method: "POST",
1041
+ body: z.object({
1042
+ referenceId: z.string().optional(),
1043
+ returnUrl: z.string().default("/"),
1044
+ }),
1045
+ use: [
1046
+ sessionMiddleware,
1047
+ originCheck((ctx) => ctx.body.returnUrl),
1048
+ referenceMiddleware("billing-portal"),
1049
+ ],
1050
+ },
1051
+ async (ctx) => {
1052
+ const { user } = ctx.context.session;
1053
+ const referenceId = ctx.body.referenceId || user.id;
1054
+
1055
+ let customerId = user.stripeCustomerId;
1056
+
1057
+ if (!customerId) {
1058
+ const subscription = await ctx.context.adapter
1059
+ .findMany<Subscription>({
1060
+ model: "subscription",
1061
+ where: [
1062
+ {
1063
+ field: "referenceId",
1064
+ value: referenceId,
1065
+ },
1066
+ ],
1067
+ })
1068
+ .then((subs) =>
1069
+ subs.find(
1070
+ (sub) => sub.status === "active" || sub.status === "trialing",
1071
+ ),
1072
+ );
1073
+
1074
+ customerId = subscription?.stripeCustomerId;
1075
+ }
1076
+
1077
+ if (!customerId) {
1078
+ throw new APIError("BAD_REQUEST", {
1079
+ message: "No Stripe customer found for this user",
1080
+ });
1081
+ }
1082
+
1083
+ try {
1084
+ const { url } = await client.billingPortal.sessions.create({
1085
+ customer: customerId,
1086
+ return_url: getUrl(ctx, ctx.body.returnUrl),
1087
+ });
1088
+
1089
+ return ctx.json({
1090
+ url,
1091
+ redirect: true,
1092
+ });
1093
+ } catch (error: any) {
1094
+ ctx.context.logger.error(
1095
+ "Error creating billing portal session",
1096
+ error,
1097
+ );
1098
+ throw new APIError("BAD_REQUEST", {
1099
+ message: error.message,
1100
+ });
1101
+ }
1102
+ },
1103
+ ),
1036
1104
  } as const;
1037
1105
  return {
1038
1106
  id: "stripe",
@@ -844,4 +844,40 @@ describe("stripe", async () => {
844
844
 
845
845
  expect(mockStripe.customers.create).toHaveBeenCalledTimes(1);
846
846
  });
847
+
848
+ it("should create billing portal session", async () => {
849
+ await authClient.signUp.email(
850
+ {
851
+ ...testUser,
852
+ email: "billing-portal@email.com",
853
+ },
854
+ {
855
+ throw: true,
856
+ },
857
+ );
858
+
859
+ const headers = new Headers();
860
+ await authClient.signIn.email(
861
+ {
862
+ ...testUser,
863
+ email: "billing-portal@email.com",
864
+ },
865
+ {
866
+ throw: true,
867
+ onSuccess: setCookieToHeader(headers),
868
+ },
869
+ );
870
+ const billingPortalRes = await authClient.subscription.billingPortal({
871
+ returnUrl: "/dashboard",
872
+ fetchOptions: {
873
+ headers,
874
+ },
875
+ });
876
+ expect(billingPortalRes.data?.url).toBe("https://billing.stripe.com/mock");
877
+ expect(billingPortalRes.data?.redirect).toBe(true);
878
+ expect(mockStripe.billingPortal.sessions.create).toHaveBeenCalledWith({
879
+ customer: expect.any(String),
880
+ return_url: "http://localhost:3000/dashboard",
881
+ });
882
+ });
847
883
  });
package/src/types.ts CHANGED
@@ -274,7 +274,8 @@ export interface StripeOptions {
274
274
  | "upgrade-subscription"
275
275
  | "list-subscription"
276
276
  | "cancel-subscription"
277
- | "restore-subscription";
277
+ | "restore-subscription"
278
+ | "billing-portal";
278
279
  },
279
280
  ctx: GenericEndpointContext,
280
281
  ) => Promise<boolean>;