@better-auth/stripe 1.4.9 → 1.4.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/.turbo/turbo-build.log +6 -6
- package/dist/client.d.mts +2 -2
- package/dist/client.mjs +2 -2
- package/dist/{index-DQa3pHPd.d.mts → index-DtwvPnmn.d.mts} +45 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +201 -59
- package/package.json +5 -5
- package/src/client.ts +1 -1
- package/src/hooks.ts +166 -17
- package/src/middleware.ts +1 -2
- package/src/routes.ts +126 -83
- package/src/schema.ts +12 -0
- package/src/stripe.test.ts +2434 -1291
- package/src/types.ts +30 -1
- package/src/utils.ts +25 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/stripe@1.4.
|
|
2
|
+
> @better-auth/stripe@1.4.10 build /home/runner/work/better-auth/better-auth/packages/stripe
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.17.2[22m powered by rolldown [2mv1.0.0-beta.53[22m
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m46.78 kB[22m [2m│ gzip: 8.45 kB[22m
|
|
11
11
|
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.26 kB[22m [2m│ gzip: 0.20 kB[22m
|
|
12
|
-
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.62 kB[22m [2m│ gzip: 0.
|
|
12
|
+
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.62 kB[22m [2m│ gzip: 0.34 kB[22m
|
|
13
13
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 0.21 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
14
|
-
[34mℹ[39m [2mdist/[22m[32mindex-
|
|
15
|
-
[34mℹ[39m 5 files, total:
|
|
16
|
-
[32m✔[39m Build complete in [
|
|
14
|
+
[34mℹ[39m [2mdist/[22m[32mindex-DtwvPnmn.d.mts[39m [2m26.30 kB[22m [2m│ gzip: 5.07 kB[22m
|
|
15
|
+
[34mℹ[39m 5 files, total: 74.17 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m16656ms[39m
|
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as stripe } from "./index-
|
|
1
|
+
import { n as stripe } from "./index-DtwvPnmn.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/client.d.ts
|
|
4
4
|
declare const stripeClient: <O extends {
|
|
@@ -17,8 +17,8 @@ declare const stripeClient: <O extends {
|
|
|
17
17
|
stripeWebhookSecret: string;
|
|
18
18
|
}>>;
|
|
19
19
|
pathMethods: {
|
|
20
|
-
"/subscription/restore": "POST";
|
|
21
20
|
"/subscription/billing-portal": "POST";
|
|
21
|
+
"/subscription/restore": "POST";
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
24
|
//#endregion
|
package/dist/client.mjs
CHANGED
|
@@ -4,8 +4,8 @@ const stripeClient = (options) => {
|
|
|
4
4
|
id: "stripe-client",
|
|
5
5
|
$InferServerPlugin: {},
|
|
6
6
|
pathMethods: {
|
|
7
|
-
"/subscription/
|
|
8
|
-
"/subscription/
|
|
7
|
+
"/subscription/billing-portal": "POST",
|
|
8
|
+
"/subscription/restore": "POST"
|
|
9
9
|
}
|
|
10
10
|
};
|
|
11
11
|
};
|
|
@@ -49,6 +49,18 @@ declare const subscriptions: {
|
|
|
49
49
|
required: false;
|
|
50
50
|
defaultValue: false;
|
|
51
51
|
};
|
|
52
|
+
cancelAt: {
|
|
53
|
+
type: "date";
|
|
54
|
+
required: false;
|
|
55
|
+
};
|
|
56
|
+
canceledAt: {
|
|
57
|
+
type: "date";
|
|
58
|
+
required: false;
|
|
59
|
+
};
|
|
60
|
+
endedAt: {
|
|
61
|
+
type: "date";
|
|
62
|
+
required: false;
|
|
63
|
+
};
|
|
52
64
|
seats: {
|
|
53
65
|
type: "number";
|
|
54
66
|
required: false;
|
|
@@ -198,9 +210,26 @@ interface Subscription {
|
|
|
198
210
|
*/
|
|
199
211
|
periodEnd?: Date | undefined;
|
|
200
212
|
/**
|
|
201
|
-
*
|
|
213
|
+
* Whether this subscription will (if status=active)
|
|
214
|
+
* or did (if status=canceled) cancel at the end of the current billing period.
|
|
202
215
|
*/
|
|
203
216
|
cancelAtPeriodEnd?: boolean | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* If the subscription is scheduled to be canceled,
|
|
219
|
+
* this is the time at which the cancellation will take effect.
|
|
220
|
+
*/
|
|
221
|
+
cancelAt?: Date | undefined;
|
|
222
|
+
/**
|
|
223
|
+
* If the subscription has been canceled, this is the time when it was canceled.
|
|
224
|
+
*
|
|
225
|
+
* Note: If the subscription was canceled with `cancel_at_period_end`,
|
|
226
|
+
* this reflects the cancellation request time, not when the subscription actually ends.
|
|
227
|
+
*/
|
|
228
|
+
canceledAt?: Date | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* If the subscription has ended, the date the subscription ended.
|
|
231
|
+
*/
|
|
232
|
+
endedAt?: Date | undefined;
|
|
204
233
|
/**
|
|
205
234
|
* A field to group subscriptions so you can have multiple subscriptions
|
|
206
235
|
* for one reference id
|
|
@@ -279,6 +308,16 @@ type SubscriptionOptions = {
|
|
|
279
308
|
stripeSubscription: Stripe.Subscription;
|
|
280
309
|
subscription: Subscription;
|
|
281
310
|
}) => Promise<void>) | undefined;
|
|
311
|
+
/**
|
|
312
|
+
* A callback to run when a subscription is created
|
|
313
|
+
* @returns
|
|
314
|
+
*/
|
|
315
|
+
onSubscriptionCreated?: ((data: {
|
|
316
|
+
event: Stripe.Event;
|
|
317
|
+
stripeSubscription: Stripe.Subscription;
|
|
318
|
+
subscription: Subscription;
|
|
319
|
+
plan: StripePlan;
|
|
320
|
+
}) => Promise<void>) | undefined;
|
|
282
321
|
/**
|
|
283
322
|
* parameters for session create params
|
|
284
323
|
*
|
|
@@ -518,6 +557,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
|
|
|
518
557
|
referenceId: zod0.ZodOptional<zod0.ZodString>;
|
|
519
558
|
subscriptionId: zod0.ZodOptional<zod0.ZodString>;
|
|
520
559
|
returnUrl: zod0.ZodString;
|
|
560
|
+
disableRedirect: zod0.ZodDefault<zod0.ZodBoolean>;
|
|
521
561
|
}, better_auth0.$strip>;
|
|
522
562
|
metadata: {
|
|
523
563
|
openapi: {
|
|
@@ -633,6 +673,9 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
|
|
|
633
673
|
periodStart?: Date | undefined;
|
|
634
674
|
periodEnd?: Date | undefined;
|
|
635
675
|
cancelAtPeriodEnd?: boolean | undefined;
|
|
676
|
+
cancelAt?: Date | undefined;
|
|
677
|
+
canceledAt?: Date | undefined;
|
|
678
|
+
endedAt?: Date | undefined;
|
|
636
679
|
groupId?: string | undefined;
|
|
637
680
|
seats?: number | undefined;
|
|
638
681
|
}[]>;
|
|
@@ -665,6 +708,7 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
|
|
|
665
708
|
locale: zod0.ZodOptional<zod0.ZodCustom<Stripe.Checkout.Session.Locale, Stripe.Checkout.Session.Locale>>;
|
|
666
709
|
referenceId: zod0.ZodOptional<zod0.ZodString>;
|
|
667
710
|
returnUrl: zod0.ZodDefault<zod0.ZodString>;
|
|
711
|
+
disableRedirect: zod0.ZodDefault<zod0.ZodBoolean>;
|
|
668
712
|
}, better_auth0.$strip>;
|
|
669
713
|
metadata: {
|
|
670
714
|
openapi: {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as SubscriptionOptions, i as Subscription, n as stripe, r as StripePlan, t as StripePlugin } from "./index-
|
|
1
|
+
import { a as SubscriptionOptions, i as Subscription, n as stripe, r as StripePlan, t as StripePlugin } from "./index-DtwvPnmn.mjs";
|
|
2
2
|
export { StripePlan, StripePlugin, Subscription, SubscriptionOptions, stripe };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineErrorCodes } from "@better-auth/core/utils";
|
|
2
2
|
import { defu } from "defu";
|
|
3
3
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
4
|
-
import { HIDE_METADATA
|
|
4
|
+
import { HIDE_METADATA } from "better-auth";
|
|
5
5
|
import { APIError, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
|
|
6
6
|
import * as z from "zod/v4";
|
|
7
7
|
import { mergeSchema } from "better-auth/db";
|
|
@@ -17,6 +17,24 @@ async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
|
|
|
17
17
|
async function getPlanByName(options, name) {
|
|
18
18
|
return await getPlans(options.subscription).then((res) => res?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()));
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a subscription is in an available state (active or trialing)
|
|
22
|
+
*/
|
|
23
|
+
function isActiveOrTrialing(sub) {
|
|
24
|
+
return sub.status === "active" || sub.status === "trialing";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if a subscription is scheduled to be canceled (DB subscription object)
|
|
28
|
+
*/
|
|
29
|
+
function isPendingCancel(sub) {
|
|
30
|
+
return !!(sub.cancelAtPeriodEnd || sub.cancelAt);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if a Stripe subscription is scheduled to be canceled (Stripe API response)
|
|
34
|
+
*/
|
|
35
|
+
function isStripePendingCancel(stripeSub) {
|
|
36
|
+
return !!(stripeSub.cancel_at_period_end || stripeSub.cancel_at);
|
|
37
|
+
}
|
|
20
38
|
|
|
21
39
|
//#endregion
|
|
22
40
|
//#region src/hooks.ts
|
|
@@ -46,6 +64,10 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
46
64
|
periodStart: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_start * 1e3),
|
|
47
65
|
periodEnd: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_end * 1e3),
|
|
48
66
|
stripeSubscriptionId: checkoutSession.subscription,
|
|
67
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
68
|
+
cancelAt: subscription.cancel_at ? /* @__PURE__ */ new Date(subscription.cancel_at * 1e3) : null,
|
|
69
|
+
canceledAt: subscription.canceled_at ? /* @__PURE__ */ new Date(subscription.canceled_at * 1e3) : null,
|
|
70
|
+
endedAt: subscription.ended_at ? /* @__PURE__ */ new Date(subscription.ended_at * 1e3) : null,
|
|
49
71
|
seats,
|
|
50
72
|
...trial
|
|
51
73
|
},
|
|
@@ -72,7 +94,81 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
96
|
} catch (e) {
|
|
75
|
-
logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
97
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function onSubscriptionCreated(ctx, options, event) {
|
|
101
|
+
try {
|
|
102
|
+
if (!options.subscription?.enabled) return;
|
|
103
|
+
const subscriptionCreated = event.data.object;
|
|
104
|
+
const stripeCustomerId = subscriptionCreated.customer?.toString();
|
|
105
|
+
if (!stripeCustomerId) {
|
|
106
|
+
ctx.context.logger.warn(`Stripe webhook warning: customer.subscription.created event received without customer ID`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (await ctx.context.adapter.findOne({
|
|
110
|
+
model: "subscription",
|
|
111
|
+
where: [{
|
|
112
|
+
field: "stripeSubscriptionId",
|
|
113
|
+
value: subscriptionCreated.id
|
|
114
|
+
}]
|
|
115
|
+
})) {
|
|
116
|
+
ctx.context.logger.info(`Stripe webhook: Subscription ${subscriptionCreated.id} already exists in database, skipping creation`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const user$1 = await ctx.context.adapter.findOne({
|
|
120
|
+
model: "user",
|
|
121
|
+
where: [{
|
|
122
|
+
field: "stripeCustomerId",
|
|
123
|
+
value: stripeCustomerId
|
|
124
|
+
}]
|
|
125
|
+
});
|
|
126
|
+
if (!user$1) {
|
|
127
|
+
ctx.context.logger.warn(`Stripe webhook warning: No user found with stripeCustomerId: ${stripeCustomerId}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const subscriptionItem = subscriptionCreated.items.data[0];
|
|
131
|
+
if (!subscriptionItem) {
|
|
132
|
+
ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const priceId = subscriptionItem.price.id;
|
|
136
|
+
const plan = await getPlanByPriceInfo(options, priceId, subscriptionItem.price.lookup_key || null);
|
|
137
|
+
if (!plan) {
|
|
138
|
+
ctx.context.logger.warn(`Stripe webhook warning: No matching plan found for priceId: ${priceId}`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const seats = subscriptionItem.quantity;
|
|
142
|
+
const periodStart = /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3);
|
|
143
|
+
const periodEnd = /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3);
|
|
144
|
+
const trial = subscriptionCreated.trial_start && subscriptionCreated.trial_end ? {
|
|
145
|
+
trialStart: /* @__PURE__ */ new Date(subscriptionCreated.trial_start * 1e3),
|
|
146
|
+
trialEnd: /* @__PURE__ */ new Date(subscriptionCreated.trial_end * 1e3)
|
|
147
|
+
} : {};
|
|
148
|
+
const newSubscription = await ctx.context.adapter.create({
|
|
149
|
+
model: "subscription",
|
|
150
|
+
data: {
|
|
151
|
+
referenceId: user$1.id,
|
|
152
|
+
stripeCustomerId,
|
|
153
|
+
stripeSubscriptionId: subscriptionCreated.id,
|
|
154
|
+
status: subscriptionCreated.status,
|
|
155
|
+
plan: plan.name.toLowerCase(),
|
|
156
|
+
periodStart,
|
|
157
|
+
periodEnd,
|
|
158
|
+
seats,
|
|
159
|
+
...plan.limits ? { limits: plan.limits } : {},
|
|
160
|
+
...trial
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
ctx.context.logger.info(`Stripe webhook: Created subscription ${subscriptionCreated.id} for user ${user$1.id} from dashboard`);
|
|
164
|
+
await options.subscription?.onSubscriptionCreated?.({
|
|
165
|
+
event,
|
|
166
|
+
subscription: newSubscription,
|
|
167
|
+
stripeSubscription: subscriptionCreated,
|
|
168
|
+
plan
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
76
172
|
}
|
|
77
173
|
}
|
|
78
174
|
async function onSubscriptionUpdated(ctx, options, event) {
|
|
@@ -102,9 +198,9 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
102
198
|
}]
|
|
103
199
|
});
|
|
104
200
|
if (subs.length > 1) {
|
|
105
|
-
const activeSub = subs.find((sub) => sub
|
|
201
|
+
const activeSub = subs.find((sub) => isActiveOrTrialing(sub));
|
|
106
202
|
if (!activeSub) {
|
|
107
|
-
logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
|
|
203
|
+
ctx.context.logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
|
|
108
204
|
return;
|
|
109
205
|
}
|
|
110
206
|
subscription = activeSub;
|
|
@@ -123,6 +219,9 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
123
219
|
periodStart: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_start * 1e3),
|
|
124
220
|
periodEnd: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_end * 1e3),
|
|
125
221
|
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
222
|
+
cancelAt: subscriptionUpdated.cancel_at ? /* @__PURE__ */ new Date(subscriptionUpdated.cancel_at * 1e3) : null,
|
|
223
|
+
canceledAt: subscriptionUpdated.canceled_at ? /* @__PURE__ */ new Date(subscriptionUpdated.canceled_at * 1e3) : null,
|
|
224
|
+
endedAt: subscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(subscriptionUpdated.ended_at * 1e3) : null,
|
|
126
225
|
seats,
|
|
127
226
|
stripeSubscriptionId: subscriptionUpdated.id
|
|
128
227
|
},
|
|
@@ -131,7 +230,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
131
230
|
value: subscription.id
|
|
132
231
|
}]
|
|
133
232
|
});
|
|
134
|
-
if (subscriptionUpdated.status === "active" && subscriptionUpdated
|
|
233
|
+
if (subscriptionUpdated.status === "active" && isStripePendingCancel(subscriptionUpdated) && !isPendingCancel(subscription)) await options.subscription.onSubscriptionCancel?.({
|
|
135
234
|
subscription,
|
|
136
235
|
cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
|
|
137
236
|
stripeSubscription: subscriptionUpdated,
|
|
@@ -146,7 +245,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
146
245
|
if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscription, ctx);
|
|
147
246
|
}
|
|
148
247
|
} catch (error) {
|
|
149
|
-
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
248
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
150
249
|
}
|
|
151
250
|
}
|
|
152
251
|
async function onSubscriptionDeleted(ctx, options, event) {
|
|
@@ -170,7 +269,11 @@ async function onSubscriptionDeleted(ctx, options, event) {
|
|
|
170
269
|
}],
|
|
171
270
|
update: {
|
|
172
271
|
status: "canceled",
|
|
173
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
272
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
273
|
+
cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
|
|
274
|
+
cancelAt: subscriptionDeleted.cancel_at ? /* @__PURE__ */ new Date(subscriptionDeleted.cancel_at * 1e3) : null,
|
|
275
|
+
canceledAt: subscriptionDeleted.canceled_at ? /* @__PURE__ */ new Date(subscriptionDeleted.canceled_at * 1e3) : null,
|
|
276
|
+
endedAt: subscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(subscriptionDeleted.ended_at * 1e3) : null
|
|
174
277
|
}
|
|
175
278
|
});
|
|
176
279
|
await options.subscription.onSubscriptionDeleted?.({
|
|
@@ -178,9 +281,9 @@ async function onSubscriptionDeleted(ctx, options, event) {
|
|
|
178
281
|
stripeSubscription: subscriptionDeleted,
|
|
179
282
|
subscription
|
|
180
283
|
});
|
|
181
|
-
} else logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
|
|
284
|
+
} else ctx.context.logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
|
|
182
285
|
} catch (error) {
|
|
183
|
-
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
286
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
184
287
|
}
|
|
185
288
|
}
|
|
186
289
|
|
|
@@ -191,7 +294,7 @@ const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddlewar
|
|
|
191
294
|
if (!session) throw new APIError("UNAUTHORIZED");
|
|
192
295
|
const referenceId = ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
|
|
193
296
|
if (referenceId !== session.user.id && !subscriptionOptions.authorizeReference) {
|
|
194
|
-
logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
|
|
297
|
+
ctx.context.logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
|
|
195
298
|
throw new APIError("BAD_REQUEST", { message: "Reference id is not allowed. Read server logs for more details." });
|
|
196
299
|
}
|
|
197
300
|
/**
|
|
@@ -314,8 +417,8 @@ const upgradeSubscription = (options) => {
|
|
|
314
417
|
value: ctx.body.referenceId || user$1.id
|
|
315
418
|
}]
|
|
316
419
|
});
|
|
317
|
-
const activeOrTrialingSubscription = subscriptions$1.find((sub) => sub
|
|
318
|
-
const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => sub
|
|
420
|
+
const activeOrTrialingSubscription = subscriptions$1.find((sub) => isActiveOrTrialing(sub));
|
|
421
|
+
const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)))).find((sub) => {
|
|
319
422
|
if (subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId) return sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId;
|
|
320
423
|
if (activeOrTrialingSubscription?.stripeSubscriptionId) return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
|
|
321
424
|
return false;
|
|
@@ -379,7 +482,7 @@ const upgradeSubscription = (options) => {
|
|
|
379
482
|
});
|
|
380
483
|
return ctx.json({
|
|
381
484
|
url,
|
|
382
|
-
redirect:
|
|
485
|
+
redirect: !ctx.body.disableRedirect
|
|
383
486
|
});
|
|
384
487
|
}
|
|
385
488
|
let subscription = activeOrTrialingSubscription || incompleteSubscription;
|
|
@@ -415,7 +518,13 @@ const upgradeSubscription = (options) => {
|
|
|
415
518
|
plan,
|
|
416
519
|
subscription
|
|
417
520
|
}, ctx.request, ctx);
|
|
418
|
-
const freeTrial = !
|
|
521
|
+
const freeTrial = !(await ctx.context.adapter.findMany({
|
|
522
|
+
model: "subscription",
|
|
523
|
+
where: [{
|
|
524
|
+
field: "referenceId",
|
|
525
|
+
value: referenceId
|
|
526
|
+
}]
|
|
527
|
+
})).some((s) => {
|
|
419
528
|
return !!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
420
529
|
}) && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
|
|
421
530
|
let priceIdToUse = void 0;
|
|
@@ -485,17 +594,19 @@ const cancelSubscriptionCallback = (options) => {
|
|
|
485
594
|
value: subscriptionId
|
|
486
595
|
}]
|
|
487
596
|
});
|
|
488
|
-
if (!subscription || subscription.
|
|
597
|
+
if (!subscription || subscription.status === "canceled" || isPendingCancel(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
489
598
|
const currentSubscription = (await client.subscriptions.list({
|
|
490
599
|
customer: user$1.stripeCustomerId,
|
|
491
600
|
status: "active"
|
|
492
601
|
})).data.find((sub) => sub.id === subscription.stripeSubscriptionId);
|
|
493
|
-
if (currentSubscription
|
|
602
|
+
if (currentSubscription && isStripePendingCancel(currentSubscription) && !isPendingCancel(subscription)) {
|
|
494
603
|
await ctx.context.adapter.update({
|
|
495
604
|
model: "subscription",
|
|
496
605
|
update: {
|
|
497
606
|
status: currentSubscription?.status,
|
|
498
|
-
cancelAtPeriodEnd:
|
|
607
|
+
cancelAtPeriodEnd: currentSubscription?.cancel_at_period_end || false,
|
|
608
|
+
cancelAt: currentSubscription?.cancel_at ? /* @__PURE__ */ new Date(currentSubscription.cancel_at * 1e3) : null,
|
|
609
|
+
canceledAt: currentSubscription?.canceled_at ? /* @__PURE__ */ new Date(currentSubscription.canceled_at * 1e3) : null
|
|
499
610
|
},
|
|
500
611
|
where: [{
|
|
501
612
|
field: "id",
|
|
@@ -518,7 +629,8 @@ const cancelSubscriptionCallback = (options) => {
|
|
|
518
629
|
const cancelSubscriptionBodySchema = z.object({
|
|
519
630
|
referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
|
|
520
631
|
subscriptionId: z.string().meta({ description: "The Stripe subscription ID to cancel. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
|
|
521
|
-
returnUrl: z.string().meta({ description: "URL to take customers to when they click on the billing portal's link to return to your website. Eg: \"/account\"" })
|
|
632
|
+
returnUrl: z.string().meta({ description: "URL to take customers to when they click on the billing portal's link to return to your website. Eg: \"/account\"" }),
|
|
633
|
+
disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription cancellation. Eg: true" }).default(false)
|
|
522
634
|
});
|
|
523
635
|
/**
|
|
524
636
|
* ### Endpoint
|
|
@@ -561,10 +673,10 @@ const cancelSubscription = (options) => {
|
|
|
561
673
|
field: "referenceId",
|
|
562
674
|
value: referenceId
|
|
563
675
|
}]
|
|
564
|
-
}).then((subs) => subs.find((sub) => sub
|
|
676
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
565
677
|
if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
|
|
566
678
|
if (!subscription || !subscription.stripeCustomerId) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
|
|
567
|
-
const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub
|
|
679
|
+
const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
|
|
568
680
|
if (!activeSubscriptions.length) {
|
|
569
681
|
/**
|
|
570
682
|
* If the subscription is not found, we need to delete the subscription
|
|
@@ -589,29 +701,36 @@ const cancelSubscription = (options) => {
|
|
|
589
701
|
subscription_cancel: { subscription: activeSubscription.id }
|
|
590
702
|
}
|
|
591
703
|
}).catch(async (e) => {
|
|
592
|
-
if (e.message
|
|
704
|
+
if (e.message?.includes("already set to be canceled")) {
|
|
593
705
|
/**
|
|
594
|
-
* in-case we missed the event from stripe, we
|
|
706
|
+
* in-case we missed the event from stripe, we sync the actual state
|
|
595
707
|
* this is a rare case and should not happen
|
|
596
708
|
*/
|
|
597
|
-
if (!subscription
|
|
598
|
-
|
|
599
|
-
update
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
709
|
+
if (!isPendingCancel(subscription)) {
|
|
710
|
+
const stripeSub = await client.subscriptions.retrieve(activeSubscription.id);
|
|
711
|
+
await ctx.context.adapter.update({
|
|
712
|
+
model: "subscription",
|
|
713
|
+
update: {
|
|
714
|
+
cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
|
|
715
|
+
cancelAt: stripeSub.cancel_at ? /* @__PURE__ */ new Date(stripeSub.cancel_at * 1e3) : null,
|
|
716
|
+
canceledAt: stripeSub.canceled_at ? /* @__PURE__ */ new Date(stripeSub.canceled_at * 1e3) : null
|
|
717
|
+
},
|
|
718
|
+
where: [{
|
|
719
|
+
field: "id",
|
|
720
|
+
value: subscription.id
|
|
721
|
+
}]
|
|
722
|
+
});
|
|
723
|
+
}
|
|
605
724
|
}
|
|
606
725
|
throw ctx.error("BAD_REQUEST", {
|
|
607
726
|
message: e.message,
|
|
608
727
|
code: e.code
|
|
609
728
|
});
|
|
610
729
|
});
|
|
611
|
-
return {
|
|
730
|
+
return ctx.json({
|
|
612
731
|
url,
|
|
613
|
-
redirect:
|
|
614
|
-
};
|
|
732
|
+
redirect: !ctx.body.disableRedirect
|
|
733
|
+
});
|
|
615
734
|
});
|
|
616
735
|
};
|
|
617
736
|
const restoreSubscriptionBodySchema = z.object({
|
|
@@ -640,31 +759,36 @@ const restoreSubscription = (options) => {
|
|
|
640
759
|
field: "referenceId",
|
|
641
760
|
value: referenceId
|
|
642
761
|
}]
|
|
643
|
-
}).then((subs) => subs.find((sub) => sub
|
|
762
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
644
763
|
if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
|
|
645
764
|
if (!subscription || !subscription.stripeCustomerId) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
|
|
646
765
|
if (subscription.status != "active" && subscription.status != "trialing") throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_ACTIVE });
|
|
647
|
-
if (!subscription
|
|
648
|
-
const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub
|
|
766
|
+
if (!isPendingCancel(subscription)) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION });
|
|
767
|
+
const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
|
|
649
768
|
if (!activeSubscription) throw ctx.error("BAD_REQUEST", { message: STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND });
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
},
|
|
658
|
-
where: [{
|
|
659
|
-
field: "id",
|
|
660
|
-
value: subscription.id
|
|
661
|
-
}]
|
|
769
|
+
const updateParams = {};
|
|
770
|
+
if (activeSubscription.cancel_at) updateParams.cancel_at = "";
|
|
771
|
+
else if (activeSubscription.cancel_at_period_end) updateParams.cancel_at_period_end = false;
|
|
772
|
+
const newSub = await client.subscriptions.update(activeSubscription.id, updateParams).catch((e) => {
|
|
773
|
+
throw ctx.error("BAD_REQUEST", {
|
|
774
|
+
message: e.message,
|
|
775
|
+
code: e.code
|
|
662
776
|
});
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
777
|
+
});
|
|
778
|
+
await ctx.context.adapter.update({
|
|
779
|
+
model: "subscription",
|
|
780
|
+
update: {
|
|
781
|
+
cancelAtPeriodEnd: false,
|
|
782
|
+
cancelAt: null,
|
|
783
|
+
canceledAt: null,
|
|
784
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
785
|
+
},
|
|
786
|
+
where: [{
|
|
787
|
+
field: "id",
|
|
788
|
+
value: subscription.id
|
|
789
|
+
}]
|
|
790
|
+
});
|
|
791
|
+
return ctx.json(newSub);
|
|
668
792
|
});
|
|
669
793
|
};
|
|
670
794
|
const listActiveSubscriptionsQuerySchema = z.optional(z.object({ referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional() }));
|
|
@@ -708,9 +832,7 @@ const listActiveSubscriptions = (options) => {
|
|
|
708
832
|
limits: plan?.limits,
|
|
709
833
|
priceId: plan?.priceId
|
|
710
834
|
};
|
|
711
|
-
}).filter((sub) =>
|
|
712
|
-
return sub.status === "active" || sub.status === "trialing";
|
|
713
|
-
});
|
|
835
|
+
}).filter((sub) => isActiveOrTrialing(sub));
|
|
714
836
|
return ctx.json(subs);
|
|
715
837
|
});
|
|
716
838
|
};
|
|
@@ -753,6 +875,9 @@ const subscriptionSuccess = (options) => {
|
|
|
753
875
|
periodEnd: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_end * 1e3),
|
|
754
876
|
periodStart: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_start * 1e3),
|
|
755
877
|
stripeSubscriptionId: stripeSubscription.id,
|
|
878
|
+
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
|
879
|
+
cancelAt: stripeSubscription.cancel_at ? /* @__PURE__ */ new Date(stripeSubscription.cancel_at * 1e3) : null,
|
|
880
|
+
canceledAt: stripeSubscription.canceled_at ? /* @__PURE__ */ new Date(stripeSubscription.canceled_at * 1e3) : null,
|
|
756
881
|
...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
|
|
757
882
|
trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
|
|
758
883
|
trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
|
|
@@ -775,7 +900,8 @@ const createBillingPortalBodySchema = z.object({
|
|
|
775
900
|
return typeof localization === "string";
|
|
776
901
|
}).optional(),
|
|
777
902
|
referenceId: z.string().optional(),
|
|
778
|
-
returnUrl: z.string().default("/")
|
|
903
|
+
returnUrl: z.string().default("/"),
|
|
904
|
+
disableRedirect: z.boolean().meta({ description: "Disable redirect after creating billing portal session. Eg: true" }).default(false)
|
|
779
905
|
});
|
|
780
906
|
const createBillingPortal = (options) => {
|
|
781
907
|
const client = options.stripeClient;
|
|
@@ -799,7 +925,7 @@ const createBillingPortal = (options) => {
|
|
|
799
925
|
field: "referenceId",
|
|
800
926
|
value: referenceId
|
|
801
927
|
}]
|
|
802
|
-
}).then((subs) => subs.find((sub) => sub
|
|
928
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
|
|
803
929
|
if (!customerId) throw new APIError("BAD_REQUEST", { message: "No Stripe customer found for this user" });
|
|
804
930
|
try {
|
|
805
931
|
const { url } = await client.billingPortal.sessions.create({
|
|
@@ -809,7 +935,7 @@ const createBillingPortal = (options) => {
|
|
|
809
935
|
});
|
|
810
936
|
return ctx.json({
|
|
811
937
|
url,
|
|
812
|
-
redirect:
|
|
938
|
+
redirect: !ctx.body.disableRedirect
|
|
813
939
|
});
|
|
814
940
|
} catch (error) {
|
|
815
941
|
ctx.context.logger.error("Error creating billing portal session", error);
|
|
@@ -848,6 +974,10 @@ const stripeWebhook = (options) => {
|
|
|
848
974
|
await onCheckoutSessionCompleted(ctx, options, event);
|
|
849
975
|
await options.onEvent?.(event);
|
|
850
976
|
break;
|
|
977
|
+
case "customer.subscription.created":
|
|
978
|
+
await onSubscriptionCreated(ctx, options, event);
|
|
979
|
+
await options.onEvent?.(event);
|
|
980
|
+
break;
|
|
851
981
|
case "customer.subscription.updated":
|
|
852
982
|
await onSubscriptionUpdated(ctx, options, event);
|
|
853
983
|
await options.onEvent?.(event);
|
|
@@ -924,6 +1054,18 @@ const subscriptions = { subscription: { fields: {
|
|
|
924
1054
|
required: false,
|
|
925
1055
|
defaultValue: false
|
|
926
1056
|
},
|
|
1057
|
+
cancelAt: {
|
|
1058
|
+
type: "date",
|
|
1059
|
+
required: false
|
|
1060
|
+
},
|
|
1061
|
+
canceledAt: {
|
|
1062
|
+
type: "date",
|
|
1063
|
+
required: false
|
|
1064
|
+
},
|
|
1065
|
+
endedAt: {
|
|
1066
|
+
type: "date",
|
|
1067
|
+
required: false
|
|
1068
|
+
},
|
|
927
1069
|
seats: {
|
|
928
1070
|
type: "number",
|
|
929
1071
|
required: false
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/stripe",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.mts",
|
|
@@ -50,15 +50,15 @@
|
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"stripe": "^18 || ^19 || ^20",
|
|
53
|
-
"
|
|
54
|
-
"better-auth": "1.4.
|
|
53
|
+
"better-auth": "1.4.10",
|
|
54
|
+
"@better-auth/core": "1.4.10"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"better-call": "1.1.7",
|
|
58
58
|
"stripe": "^20.0.0",
|
|
59
59
|
"tsdown": "^0.17.2",
|
|
60
|
-
"@better-auth/core": "1.4.
|
|
61
|
-
"better-auth": "1.4.
|
|
60
|
+
"@better-auth/core": "1.4.10",
|
|
61
|
+
"better-auth": "1.4.10"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"test": "vitest",
|