@better-auth/stripe 1.4.10-beta.1 → 1.5.0-beta.2
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 +7 -7
- package/dist/client.d.mts +2 -1
- package/dist/client.mjs +4 -1
- package/dist/{index-DpiQGYLJ.d.mts → index-SbT5j9k6.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 -0
- package/src/hooks.ts +166 -17
- package/src/middleware.ts +1 -2
- package/src/routes.ts +126 -84
- 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.
|
|
2
|
+
> @better-auth/stripe@1.5.0-beta.2 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,11 +7,11 @@
|
|
|
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 [
|
|
11
|
-
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m46.22 kB[22m [2m│ gzip: 8.51 kB[22m
|
|
11
|
+
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.39 kB[22m [2m│ gzip: 0.27 kB[22m
|
|
12
12
|
[34mℹ[39m [2mdist/[22merror-codes-qqooUh6R.mjs [2m 0.72 kB[22m [2m│ gzip: 0.42 kB[22m
|
|
13
|
-
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m
|
|
13
|
+
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 3.01 kB[22m [2m│ gzip: 0.80 kB[22m
|
|
14
14
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 0.21 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
15
|
-
[34mℹ[39m [2mdist/[22m[32mindex-
|
|
16
|
-
[34mℹ[39m 6 files, total:
|
|
17
|
-
[32m✔[39m Build complete in [
|
|
15
|
+
[34mℹ[39m [2mdist/[22m[32mindex-SbT5j9k6.d.mts[39m [2m26.83 kB[22m [2m│ gzip: 5.12 kB[22m
|
|
16
|
+
[34mℹ[39m 6 files, total: 77.38 kB
|
|
17
|
+
[32m✔[39m Build complete in [32m16492ms[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-SbT5j9k6.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/error-codes.d.ts
|
|
4
4
|
declare const STRIPE_ERROR_CODES: {
|
|
@@ -54,6 +54,7 @@ declare const stripeClient: <O extends {
|
|
|
54
54
|
}>>;
|
|
55
55
|
pathMethods: {
|
|
56
56
|
"/subscription/billing-portal": "POST";
|
|
57
|
+
"/subscription/restore": "POST";
|
|
57
58
|
};
|
|
58
59
|
$ERROR_CODES: {
|
|
59
60
|
readonly SUBSCRIPTION_NOT_FOUND: {
|
package/dist/client.mjs
CHANGED
|
@@ -5,7 +5,10 @@ const stripeClient = (options) => {
|
|
|
5
5
|
return {
|
|
6
6
|
id: "stripe-client",
|
|
7
7
|
$InferServerPlugin: {},
|
|
8
|
-
pathMethods: {
|
|
8
|
+
pathMethods: {
|
|
9
|
+
"/subscription/billing-portal": "POST",
|
|
10
|
+
"/subscription/restore": "POST"
|
|
11
|
+
},
|
|
9
12
|
$ERROR_CODES: STRIPE_ERROR_CODES
|
|
10
13
|
};
|
|
11
14
|
};
|
|
@@ -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-SbT5j9k6.mjs";
|
|
2
2
|
export { StripePlan, StripePlugin, Subscription, SubscriptionOptions, stripe };
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { defineErrorCodes } from "@better-auth/core/utils";
|
|
|
3
3
|
import { defu } from "defu";
|
|
4
4
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
5
5
|
import { APIError } from "@better-auth/core/error";
|
|
6
|
-
import { HIDE_METADATA
|
|
6
|
+
import { HIDE_METADATA } from "better-auth";
|
|
7
7
|
import { APIError as APIError$1, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
9
|
import { mergeSchema } from "better-auth/db";
|
|
@@ -19,6 +19,24 @@ async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
|
|
|
19
19
|
async function getPlanByName(options, name) {
|
|
20
20
|
return await getPlans(options.subscription).then((res) => res?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()));
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Checks if a subscription is in an available state (active or trialing)
|
|
24
|
+
*/
|
|
25
|
+
function isActiveOrTrialing(sub) {
|
|
26
|
+
return sub.status === "active" || sub.status === "trialing";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a subscription is scheduled to be canceled (DB subscription object)
|
|
30
|
+
*/
|
|
31
|
+
function isPendingCancel(sub) {
|
|
32
|
+
return !!(sub.cancelAtPeriodEnd || sub.cancelAt);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if a Stripe subscription is scheduled to be canceled (Stripe API response)
|
|
36
|
+
*/
|
|
37
|
+
function isStripePendingCancel(stripeSub) {
|
|
38
|
+
return !!(stripeSub.cancel_at_period_end || stripeSub.cancel_at);
|
|
39
|
+
}
|
|
22
40
|
|
|
23
41
|
//#endregion
|
|
24
42
|
//#region src/hooks.ts
|
|
@@ -48,6 +66,10 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
48
66
|
periodStart: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_start * 1e3),
|
|
49
67
|
periodEnd: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_end * 1e3),
|
|
50
68
|
stripeSubscriptionId: checkoutSession.subscription,
|
|
69
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
70
|
+
cancelAt: subscription.cancel_at ? /* @__PURE__ */ new Date(subscription.cancel_at * 1e3) : null,
|
|
71
|
+
canceledAt: subscription.canceled_at ? /* @__PURE__ */ new Date(subscription.canceled_at * 1e3) : null,
|
|
72
|
+
endedAt: subscription.ended_at ? /* @__PURE__ */ new Date(subscription.ended_at * 1e3) : null,
|
|
51
73
|
seats,
|
|
52
74
|
...trial
|
|
53
75
|
},
|
|
@@ -74,7 +96,81 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
74
96
|
}
|
|
75
97
|
}
|
|
76
98
|
} catch (e) {
|
|
77
|
-
logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
99
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function onSubscriptionCreated(ctx, options, event) {
|
|
103
|
+
try {
|
|
104
|
+
if (!options.subscription?.enabled) return;
|
|
105
|
+
const subscriptionCreated = event.data.object;
|
|
106
|
+
const stripeCustomerId = subscriptionCreated.customer?.toString();
|
|
107
|
+
if (!stripeCustomerId) {
|
|
108
|
+
ctx.context.logger.warn(`Stripe webhook warning: customer.subscription.created event received without customer ID`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (await ctx.context.adapter.findOne({
|
|
112
|
+
model: "subscription",
|
|
113
|
+
where: [{
|
|
114
|
+
field: "stripeSubscriptionId",
|
|
115
|
+
value: subscriptionCreated.id
|
|
116
|
+
}]
|
|
117
|
+
})) {
|
|
118
|
+
ctx.context.logger.info(`Stripe webhook: Subscription ${subscriptionCreated.id} already exists in database, skipping creation`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const user$1 = await ctx.context.adapter.findOne({
|
|
122
|
+
model: "user",
|
|
123
|
+
where: [{
|
|
124
|
+
field: "stripeCustomerId",
|
|
125
|
+
value: stripeCustomerId
|
|
126
|
+
}]
|
|
127
|
+
});
|
|
128
|
+
if (!user$1) {
|
|
129
|
+
ctx.context.logger.warn(`Stripe webhook warning: No user found with stripeCustomerId: ${stripeCustomerId}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const subscriptionItem = subscriptionCreated.items.data[0];
|
|
133
|
+
if (!subscriptionItem) {
|
|
134
|
+
ctx.context.logger.warn(`Stripe webhook warning: Subscription ${subscriptionCreated.id} has no items`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const priceId = subscriptionItem.price.id;
|
|
138
|
+
const plan = await getPlanByPriceInfo(options, priceId, subscriptionItem.price.lookup_key || null);
|
|
139
|
+
if (!plan) {
|
|
140
|
+
ctx.context.logger.warn(`Stripe webhook warning: No matching plan found for priceId: ${priceId}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const seats = subscriptionItem.quantity;
|
|
144
|
+
const periodStart = /* @__PURE__ */ new Date(subscriptionItem.current_period_start * 1e3);
|
|
145
|
+
const periodEnd = /* @__PURE__ */ new Date(subscriptionItem.current_period_end * 1e3);
|
|
146
|
+
const trial = subscriptionCreated.trial_start && subscriptionCreated.trial_end ? {
|
|
147
|
+
trialStart: /* @__PURE__ */ new Date(subscriptionCreated.trial_start * 1e3),
|
|
148
|
+
trialEnd: /* @__PURE__ */ new Date(subscriptionCreated.trial_end * 1e3)
|
|
149
|
+
} : {};
|
|
150
|
+
const newSubscription = await ctx.context.adapter.create({
|
|
151
|
+
model: "subscription",
|
|
152
|
+
data: {
|
|
153
|
+
referenceId: user$1.id,
|
|
154
|
+
stripeCustomerId,
|
|
155
|
+
stripeSubscriptionId: subscriptionCreated.id,
|
|
156
|
+
status: subscriptionCreated.status,
|
|
157
|
+
plan: plan.name.toLowerCase(),
|
|
158
|
+
periodStart,
|
|
159
|
+
periodEnd,
|
|
160
|
+
seats,
|
|
161
|
+
...plan.limits ? { limits: plan.limits } : {},
|
|
162
|
+
...trial
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
ctx.context.logger.info(`Stripe webhook: Created subscription ${subscriptionCreated.id} for user ${user$1.id} from dashboard`);
|
|
166
|
+
await options.subscription?.onSubscriptionCreated?.({
|
|
167
|
+
event,
|
|
168
|
+
subscription: newSubscription,
|
|
169
|
+
stripeSubscription: subscriptionCreated,
|
|
170
|
+
plan
|
|
171
|
+
});
|
|
172
|
+
} catch (error) {
|
|
173
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
78
174
|
}
|
|
79
175
|
}
|
|
80
176
|
async function onSubscriptionUpdated(ctx, options, event) {
|
|
@@ -104,9 +200,9 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
104
200
|
}]
|
|
105
201
|
});
|
|
106
202
|
if (subs.length > 1) {
|
|
107
|
-
const activeSub = subs.find((sub) => sub
|
|
203
|
+
const activeSub = subs.find((sub) => isActiveOrTrialing(sub));
|
|
108
204
|
if (!activeSub) {
|
|
109
|
-
logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
|
|
205
|
+
ctx.context.logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
|
|
110
206
|
return;
|
|
111
207
|
}
|
|
112
208
|
subscription = activeSub;
|
|
@@ -125,6 +221,9 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
125
221
|
periodStart: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_start * 1e3),
|
|
126
222
|
periodEnd: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_end * 1e3),
|
|
127
223
|
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
224
|
+
cancelAt: subscriptionUpdated.cancel_at ? /* @__PURE__ */ new Date(subscriptionUpdated.cancel_at * 1e3) : null,
|
|
225
|
+
canceledAt: subscriptionUpdated.canceled_at ? /* @__PURE__ */ new Date(subscriptionUpdated.canceled_at * 1e3) : null,
|
|
226
|
+
endedAt: subscriptionUpdated.ended_at ? /* @__PURE__ */ new Date(subscriptionUpdated.ended_at * 1e3) : null,
|
|
128
227
|
seats,
|
|
129
228
|
stripeSubscriptionId: subscriptionUpdated.id
|
|
130
229
|
},
|
|
@@ -133,7 +232,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
133
232
|
value: subscription.id
|
|
134
233
|
}]
|
|
135
234
|
});
|
|
136
|
-
if (subscriptionUpdated.status === "active" && subscriptionUpdated
|
|
235
|
+
if (subscriptionUpdated.status === "active" && isStripePendingCancel(subscriptionUpdated) && !isPendingCancel(subscription)) await options.subscription.onSubscriptionCancel?.({
|
|
137
236
|
subscription,
|
|
138
237
|
cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
|
|
139
238
|
stripeSubscription: subscriptionUpdated,
|
|
@@ -148,7 +247,7 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
148
247
|
if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscription, ctx);
|
|
149
248
|
}
|
|
150
249
|
} catch (error) {
|
|
151
|
-
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
250
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
152
251
|
}
|
|
153
252
|
}
|
|
154
253
|
async function onSubscriptionDeleted(ctx, options, event) {
|
|
@@ -172,7 +271,11 @@ async function onSubscriptionDeleted(ctx, options, event) {
|
|
|
172
271
|
}],
|
|
173
272
|
update: {
|
|
174
273
|
status: "canceled",
|
|
175
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
274
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
275
|
+
cancelAtPeriodEnd: subscriptionDeleted.cancel_at_period_end,
|
|
276
|
+
cancelAt: subscriptionDeleted.cancel_at ? /* @__PURE__ */ new Date(subscriptionDeleted.cancel_at * 1e3) : null,
|
|
277
|
+
canceledAt: subscriptionDeleted.canceled_at ? /* @__PURE__ */ new Date(subscriptionDeleted.canceled_at * 1e3) : null,
|
|
278
|
+
endedAt: subscriptionDeleted.ended_at ? /* @__PURE__ */ new Date(subscriptionDeleted.ended_at * 1e3) : null
|
|
176
279
|
}
|
|
177
280
|
});
|
|
178
281
|
await options.subscription.onSubscriptionDeleted?.({
|
|
@@ -180,9 +283,9 @@ async function onSubscriptionDeleted(ctx, options, event) {
|
|
|
180
283
|
stripeSubscription: subscriptionDeleted,
|
|
181
284
|
subscription
|
|
182
285
|
});
|
|
183
|
-
} else logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
|
|
286
|
+
} else ctx.context.logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
|
|
184
287
|
} catch (error) {
|
|
185
|
-
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
288
|
+
ctx.context.logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
186
289
|
}
|
|
187
290
|
}
|
|
188
291
|
|
|
@@ -193,7 +296,7 @@ const referenceMiddleware = (subscriptionOptions, action) => createAuthMiddlewar
|
|
|
193
296
|
if (!session) throw new APIError$1("UNAUTHORIZED");
|
|
194
297
|
const referenceId = ctx.body?.referenceId || ctx.query?.referenceId || session.user.id;
|
|
195
298
|
if (referenceId !== session.user.id && !subscriptionOptions.authorizeReference) {
|
|
196
|
-
logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
|
|
299
|
+
ctx.context.logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
|
|
197
300
|
throw new APIError$1("BAD_REQUEST", { message: "Reference id is not allowed. Read server logs for more details." });
|
|
198
301
|
}
|
|
199
302
|
/**
|
|
@@ -306,8 +409,8 @@ const upgradeSubscription = (options) => {
|
|
|
306
409
|
value: ctx.body.referenceId || user$1.id
|
|
307
410
|
}]
|
|
308
411
|
});
|
|
309
|
-
const activeOrTrialingSubscription = subscriptions$1.find((sub) => sub
|
|
310
|
-
const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => sub
|
|
412
|
+
const activeOrTrialingSubscription = subscriptions$1.find((sub) => isActiveOrTrialing(sub));
|
|
413
|
+
const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)))).find((sub) => {
|
|
311
414
|
if (subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId) return sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId;
|
|
312
415
|
if (activeOrTrialingSubscription?.stripeSubscriptionId) return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
|
|
313
416
|
return false;
|
|
@@ -371,7 +474,7 @@ const upgradeSubscription = (options) => {
|
|
|
371
474
|
});
|
|
372
475
|
return ctx.json({
|
|
373
476
|
url,
|
|
374
|
-
redirect:
|
|
477
|
+
redirect: !ctx.body.disableRedirect
|
|
375
478
|
});
|
|
376
479
|
}
|
|
377
480
|
let subscription = activeOrTrialingSubscription || incompleteSubscription;
|
|
@@ -407,7 +510,13 @@ const upgradeSubscription = (options) => {
|
|
|
407
510
|
plan,
|
|
408
511
|
subscription
|
|
409
512
|
}, ctx.request, ctx);
|
|
410
|
-
const freeTrial = !
|
|
513
|
+
const freeTrial = !(await ctx.context.adapter.findMany({
|
|
514
|
+
model: "subscription",
|
|
515
|
+
where: [{
|
|
516
|
+
field: "referenceId",
|
|
517
|
+
value: referenceId
|
|
518
|
+
}]
|
|
519
|
+
})).some((s) => {
|
|
411
520
|
return !!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
412
521
|
}) && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
|
|
413
522
|
let priceIdToUse = void 0;
|
|
@@ -477,17 +586,19 @@ const cancelSubscriptionCallback = (options) => {
|
|
|
477
586
|
value: subscriptionId
|
|
478
587
|
}]
|
|
479
588
|
});
|
|
480
|
-
if (!subscription || subscription.
|
|
589
|
+
if (!subscription || subscription.status === "canceled" || isPendingCancel(subscription)) throw ctx.redirect(getUrl(ctx, callbackURL));
|
|
481
590
|
const currentSubscription = (await client.subscriptions.list({
|
|
482
591
|
customer: user$1.stripeCustomerId,
|
|
483
592
|
status: "active"
|
|
484
593
|
})).data.find((sub) => sub.id === subscription.stripeSubscriptionId);
|
|
485
|
-
if (currentSubscription
|
|
594
|
+
if (currentSubscription && isStripePendingCancel(currentSubscription) && !isPendingCancel(subscription)) {
|
|
486
595
|
await ctx.context.adapter.update({
|
|
487
596
|
model: "subscription",
|
|
488
597
|
update: {
|
|
489
598
|
status: currentSubscription?.status,
|
|
490
|
-
cancelAtPeriodEnd:
|
|
599
|
+
cancelAtPeriodEnd: currentSubscription?.cancel_at_period_end || false,
|
|
600
|
+
cancelAt: currentSubscription?.cancel_at ? /* @__PURE__ */ new Date(currentSubscription.cancel_at * 1e3) : null,
|
|
601
|
+
canceledAt: currentSubscription?.canceled_at ? /* @__PURE__ */ new Date(currentSubscription.canceled_at * 1e3) : null
|
|
491
602
|
},
|
|
492
603
|
where: [{
|
|
493
604
|
field: "id",
|
|
@@ -510,7 +621,8 @@ const cancelSubscriptionCallback = (options) => {
|
|
|
510
621
|
const cancelSubscriptionBodySchema = z.object({
|
|
511
622
|
referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
|
|
512
623
|
subscriptionId: z.string().meta({ description: "The Stripe subscription ID to cancel. Eg: 'sub_1ABC2DEF3GHI4JKL'" }).optional(),
|
|
513
|
-
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\"" })
|
|
624
|
+
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\"" }),
|
|
625
|
+
disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription cancellation. Eg: true" }).default(false)
|
|
514
626
|
});
|
|
515
627
|
/**
|
|
516
628
|
* ### Endpoint
|
|
@@ -553,10 +665,10 @@ const cancelSubscription = (options) => {
|
|
|
553
665
|
field: "referenceId",
|
|
554
666
|
value: referenceId
|
|
555
667
|
}]
|
|
556
|
-
}).then((subs) => subs.find((sub) => sub
|
|
668
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
557
669
|
if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
|
|
558
670
|
if (!subscription || !subscription.stripeCustomerId) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
|
|
559
|
-
const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub
|
|
671
|
+
const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub)));
|
|
560
672
|
if (!activeSubscriptions.length) {
|
|
561
673
|
/**
|
|
562
674
|
* If the subscription is not found, we need to delete the subscription
|
|
@@ -581,29 +693,36 @@ const cancelSubscription = (options) => {
|
|
|
581
693
|
subscription_cancel: { subscription: activeSubscription.id }
|
|
582
694
|
}
|
|
583
695
|
}).catch(async (e) => {
|
|
584
|
-
if (e.message
|
|
696
|
+
if (e.message?.includes("already set to be canceled")) {
|
|
585
697
|
/**
|
|
586
|
-
* in-case we missed the event from stripe, we
|
|
698
|
+
* in-case we missed the event from stripe, we sync the actual state
|
|
587
699
|
* this is a rare case and should not happen
|
|
588
700
|
*/
|
|
589
|
-
if (!subscription
|
|
590
|
-
|
|
591
|
-
update
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
701
|
+
if (!isPendingCancel(subscription)) {
|
|
702
|
+
const stripeSub = await client.subscriptions.retrieve(activeSubscription.id);
|
|
703
|
+
await ctx.context.adapter.update({
|
|
704
|
+
model: "subscription",
|
|
705
|
+
update: {
|
|
706
|
+
cancelAtPeriodEnd: stripeSub.cancel_at_period_end,
|
|
707
|
+
cancelAt: stripeSub.cancel_at ? /* @__PURE__ */ new Date(stripeSub.cancel_at * 1e3) : null,
|
|
708
|
+
canceledAt: stripeSub.canceled_at ? /* @__PURE__ */ new Date(stripeSub.canceled_at * 1e3) : null
|
|
709
|
+
},
|
|
710
|
+
where: [{
|
|
711
|
+
field: "id",
|
|
712
|
+
value: subscription.id
|
|
713
|
+
}]
|
|
714
|
+
});
|
|
715
|
+
}
|
|
597
716
|
}
|
|
598
717
|
throw ctx.error("BAD_REQUEST", {
|
|
599
718
|
message: e.message,
|
|
600
719
|
code: e.code
|
|
601
720
|
});
|
|
602
721
|
});
|
|
603
|
-
return {
|
|
722
|
+
return ctx.json({
|
|
604
723
|
url,
|
|
605
|
-
redirect:
|
|
606
|
-
};
|
|
724
|
+
redirect: !ctx.body.disableRedirect
|
|
725
|
+
});
|
|
607
726
|
});
|
|
608
727
|
};
|
|
609
728
|
const restoreSubscriptionBodySchema = z.object({
|
|
@@ -632,31 +751,36 @@ const restoreSubscription = (options) => {
|
|
|
632
751
|
field: "referenceId",
|
|
633
752
|
value: referenceId
|
|
634
753
|
}]
|
|
635
|
-
}).then((subs) => subs.find((sub) => sub
|
|
754
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub)));
|
|
636
755
|
if (ctx.body.subscriptionId && subscription && subscription.referenceId !== referenceId) subscription = void 0;
|
|
637
756
|
if (!subscription || !subscription.stripeCustomerId) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
|
|
638
757
|
if (subscription.status != "active" && subscription.status != "trialing") throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_ACTIVE);
|
|
639
|
-
if (!subscription
|
|
640
|
-
const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => sub
|
|
758
|
+
if (!isPendingCancel(subscription)) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION);
|
|
759
|
+
const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res) => res.data.filter((sub) => isActiveOrTrialing(sub))[0]);
|
|
641
760
|
if (!activeSubscription) throw APIError.from("BAD_REQUEST", STRIPE_ERROR_CODES$1.SUBSCRIPTION_NOT_FOUND);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
},
|
|
650
|
-
where: [{
|
|
651
|
-
field: "id",
|
|
652
|
-
value: subscription.id
|
|
653
|
-
}]
|
|
761
|
+
const updateParams = {};
|
|
762
|
+
if (activeSubscription.cancel_at) updateParams.cancel_at = "";
|
|
763
|
+
else if (activeSubscription.cancel_at_period_end) updateParams.cancel_at_period_end = false;
|
|
764
|
+
const newSub = await client.subscriptions.update(activeSubscription.id, updateParams).catch((e) => {
|
|
765
|
+
throw ctx.error("BAD_REQUEST", {
|
|
766
|
+
message: e.message,
|
|
767
|
+
code: e.code
|
|
654
768
|
});
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
769
|
+
});
|
|
770
|
+
await ctx.context.adapter.update({
|
|
771
|
+
model: "subscription",
|
|
772
|
+
update: {
|
|
773
|
+
cancelAtPeriodEnd: false,
|
|
774
|
+
cancelAt: null,
|
|
775
|
+
canceledAt: null,
|
|
776
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
777
|
+
},
|
|
778
|
+
where: [{
|
|
779
|
+
field: "id",
|
|
780
|
+
value: subscription.id
|
|
781
|
+
}]
|
|
782
|
+
});
|
|
783
|
+
return ctx.json(newSub);
|
|
660
784
|
});
|
|
661
785
|
};
|
|
662
786
|
const listActiveSubscriptionsQuerySchema = z.optional(z.object({ referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional() }));
|
|
@@ -700,9 +824,7 @@ const listActiveSubscriptions = (options) => {
|
|
|
700
824
|
limits: plan?.limits,
|
|
701
825
|
priceId: plan?.priceId
|
|
702
826
|
};
|
|
703
|
-
}).filter((sub) =>
|
|
704
|
-
return sub.status === "active" || sub.status === "trialing";
|
|
705
|
-
});
|
|
827
|
+
}).filter((sub) => isActiveOrTrialing(sub));
|
|
706
828
|
return ctx.json(subs);
|
|
707
829
|
});
|
|
708
830
|
};
|
|
@@ -745,6 +867,9 @@ const subscriptionSuccess = (options) => {
|
|
|
745
867
|
periodEnd: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_end * 1e3),
|
|
746
868
|
periodStart: /* @__PURE__ */ new Date(stripeSubscription.items.data[0]?.current_period_start * 1e3),
|
|
747
869
|
stripeSubscriptionId: stripeSubscription.id,
|
|
870
|
+
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
|
871
|
+
cancelAt: stripeSubscription.cancel_at ? /* @__PURE__ */ new Date(stripeSubscription.cancel_at * 1e3) : null,
|
|
872
|
+
canceledAt: stripeSubscription.canceled_at ? /* @__PURE__ */ new Date(stripeSubscription.canceled_at * 1e3) : null,
|
|
748
873
|
...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
|
|
749
874
|
trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
|
|
750
875
|
trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
|
|
@@ -767,7 +892,8 @@ const createBillingPortalBodySchema = z.object({
|
|
|
767
892
|
return typeof localization === "string";
|
|
768
893
|
}).optional(),
|
|
769
894
|
referenceId: z.string().optional(),
|
|
770
|
-
returnUrl: z.string().default("/")
|
|
895
|
+
returnUrl: z.string().default("/"),
|
|
896
|
+
disableRedirect: z.boolean().meta({ description: "Disable redirect after creating billing portal session. Eg: true" }).default(false)
|
|
771
897
|
});
|
|
772
898
|
const createBillingPortal = (options) => {
|
|
773
899
|
const client = options.stripeClient;
|
|
@@ -791,7 +917,7 @@ const createBillingPortal = (options) => {
|
|
|
791
917
|
field: "referenceId",
|
|
792
918
|
value: referenceId
|
|
793
919
|
}]
|
|
794
|
-
}).then((subs) => subs.find((sub) => sub
|
|
920
|
+
}).then((subs) => subs.find((sub) => isActiveOrTrialing(sub))))?.stripeCustomerId;
|
|
795
921
|
if (!customerId) throw new APIError("BAD_REQUEST", { message: "No Stripe customer found for this user" });
|
|
796
922
|
try {
|
|
797
923
|
const { url } = await client.billingPortal.sessions.create({
|
|
@@ -801,7 +927,7 @@ const createBillingPortal = (options) => {
|
|
|
801
927
|
});
|
|
802
928
|
return ctx.json({
|
|
803
929
|
url,
|
|
804
|
-
redirect:
|
|
930
|
+
redirect: !ctx.body.disableRedirect
|
|
805
931
|
});
|
|
806
932
|
} catch (error) {
|
|
807
933
|
ctx.context.logger.error("Error creating billing portal session", error);
|
|
@@ -840,6 +966,10 @@ const stripeWebhook = (options) => {
|
|
|
840
966
|
await onCheckoutSessionCompleted(ctx, options, event);
|
|
841
967
|
await options.onEvent?.(event);
|
|
842
968
|
break;
|
|
969
|
+
case "customer.subscription.created":
|
|
970
|
+
await onSubscriptionCreated(ctx, options, event);
|
|
971
|
+
await options.onEvent?.(event);
|
|
972
|
+
break;
|
|
843
973
|
case "customer.subscription.updated":
|
|
844
974
|
await onSubscriptionUpdated(ctx, options, event);
|
|
845
975
|
await options.onEvent?.(event);
|
|
@@ -916,6 +1046,18 @@ const subscriptions = { subscription: { fields: {
|
|
|
916
1046
|
required: false,
|
|
917
1047
|
defaultValue: false
|
|
918
1048
|
},
|
|
1049
|
+
cancelAt: {
|
|
1050
|
+
type: "date",
|
|
1051
|
+
required: false
|
|
1052
|
+
},
|
|
1053
|
+
canceledAt: {
|
|
1054
|
+
type: "date",
|
|
1055
|
+
required: false
|
|
1056
|
+
},
|
|
1057
|
+
endedAt: {
|
|
1058
|
+
type: "date",
|
|
1059
|
+
required: false
|
|
1060
|
+
},
|
|
919
1061
|
seats: {
|
|
920
1062
|
type: "number",
|
|
921
1063
|
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
|
+
"version": "1.5.0-beta.2",
|
|
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
|
-
"@better-auth/core": "1.
|
|
54
|
-
"better-auth": "1.
|
|
53
|
+
"@better-auth/core": "1.5.0-beta.2",
|
|
54
|
+
"better-auth": "1.5.0-beta.2"
|
|
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.
|
|
61
|
-
"better-auth": "1.
|
|
60
|
+
"@better-auth/core": "1.5.0-beta.2",
|
|
61
|
+
"better-auth": "1.5.0-beta.2"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"test": "vitest",
|