@better-auth/stripe 1.2.1-beta.2 → 1.2.1-beta.4
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 +4 -4
- package/dist/index.cjs +149 -122
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +149 -122
- package/package.json +2 -2
- package/src/hooks.ts +171 -132
- package/src/stripe.test.ts +228 -0
- package/src/types.ts +4 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/stripe@1.2.1-beta.
|
|
2
|
+
> @better-auth/stripe@1.2.1-beta.4 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:
|
|
8
|
+
[log] dist/index.cjs (total size: 26 kB, chunk size: 26 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:
|
|
12
|
+
[log] dist/index.mjs (total size: 25.8 kB, chunk size: 25.8 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):
|
|
16
|
+
Σ Total dist size (byte size): 159 kB
|
|
17
17
|
[log]
|
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ const plugins = require('better-auth/plugins');
|
|
|
4
4
|
const zod = require('zod');
|
|
5
5
|
const api = require('better-auth/api');
|
|
6
6
|
const crypto = require('better-auth/crypto');
|
|
7
|
+
const betterAuth = require('better-auth');
|
|
7
8
|
|
|
8
9
|
async function getPlans(options) {
|
|
9
10
|
return typeof options?.subscription?.plans === "function" ? await options.subscription?.plans() : options.subscription?.plans;
|
|
@@ -20,46 +21,37 @@ async function getPlanByName(options, name) {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
update: {
|
|
45
|
-
plan: plan.name.toLowerCase(),
|
|
46
|
-
status: subscription.status,
|
|
47
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
48
|
-
periodStart: new Date(subscription.current_period_start * 1e3),
|
|
49
|
-
periodEnd: new Date(subscription.current_period_end * 1e3),
|
|
50
|
-
seats,
|
|
51
|
-
...trial
|
|
52
|
-
},
|
|
53
|
-
where: [
|
|
54
|
-
{
|
|
55
|
-
field: "id",
|
|
56
|
-
value: subscriptionId
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
});
|
|
60
|
-
if (!dbSubscription) {
|
|
61
|
-
dbSubscription = await ctx.context.adapter.findOne({
|
|
24
|
+
try {
|
|
25
|
+
const client = options.stripeClient;
|
|
26
|
+
const checkoutSession = event.data.object;
|
|
27
|
+
if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const subscription = await client.subscriptions.retrieve(
|
|
31
|
+
checkoutSession.subscription
|
|
32
|
+
);
|
|
33
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
34
|
+
const plan = await getPlanByPriceId(options, priceId);
|
|
35
|
+
if (plan) {
|
|
36
|
+
const referenceId = checkoutSession?.metadata?.referenceId;
|
|
37
|
+
const subscriptionId = checkoutSession?.metadata?.subscriptionId;
|
|
38
|
+
const seats = subscription.items.data[0].quantity;
|
|
39
|
+
if (referenceId && subscriptionId) {
|
|
40
|
+
const trial = subscription.trial_start && subscription.trial_end ? {
|
|
41
|
+
trialStart: new Date(subscription.trial_start * 1e3),
|
|
42
|
+
trialEnd: new Date(subscription.trial_end * 1e3)
|
|
43
|
+
} : {};
|
|
44
|
+
let dbSubscription = await ctx.context.adapter.update({
|
|
62
45
|
model: "subscription",
|
|
46
|
+
update: {
|
|
47
|
+
plan: plan.name.toLowerCase(),
|
|
48
|
+
status: subscription.status,
|
|
49
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
50
|
+
periodStart: new Date(subscription.current_period_start * 1e3),
|
|
51
|
+
periodEnd: new Date(subscription.current_period_end * 1e3),
|
|
52
|
+
seats,
|
|
53
|
+
...trial
|
|
54
|
+
},
|
|
63
55
|
where: [
|
|
64
56
|
{
|
|
65
57
|
field: "id",
|
|
@@ -67,111 +59,146 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
67
59
|
}
|
|
68
60
|
]
|
|
69
61
|
});
|
|
62
|
+
if (trial.trialStart && plan.freeTrial?.onTrialStart) {
|
|
63
|
+
await plan.freeTrial.onTrialStart(dbSubscription);
|
|
64
|
+
}
|
|
65
|
+
if (!dbSubscription) {
|
|
66
|
+
dbSubscription = await ctx.context.adapter.findOne({
|
|
67
|
+
model: "subscription",
|
|
68
|
+
where: [
|
|
69
|
+
{
|
|
70
|
+
field: "id",
|
|
71
|
+
value: subscriptionId
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
await options.subscription?.onSubscriptionComplete?.({
|
|
77
|
+
event,
|
|
78
|
+
subscription: dbSubscription,
|
|
79
|
+
stripeSubscription: subscription,
|
|
80
|
+
plan
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
70
83
|
}
|
|
71
|
-
await options.subscription?.onSubscriptionComplete?.({
|
|
72
|
-
event,
|
|
73
|
-
subscription: dbSubscription,
|
|
74
|
-
stripeSubscription: subscription,
|
|
75
|
-
plan
|
|
76
|
-
});
|
|
77
|
-
return;
|
|
78
84
|
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
betterAuth.logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
async function onSubscriptionUpdated(ctx, options, event) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
const subscriptionUpdated = event.data.object;
|
|
86
|
-
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
87
|
-
const plan = await getPlanByPriceId(options, priceId);
|
|
88
|
-
if (plan) {
|
|
89
|
-
const stripeId = subscriptionUpdated.customer.toString();
|
|
90
|
-
const subscription = await ctx.context.adapter.findOne({
|
|
91
|
-
model: "subscription",
|
|
92
|
-
where: [
|
|
93
|
-
{
|
|
94
|
-
field: "stripeSubscriptionId",
|
|
95
|
-
value: stripeId
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
});
|
|
99
|
-
if (!subscription) {
|
|
90
|
+
try {
|
|
91
|
+
if (!options.subscription?.enabled) {
|
|
100
92
|
return;
|
|
101
93
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
where: [
|
|
116
|
-
{
|
|
117
|
-
field: "stripeSubscriptionId",
|
|
118
|
-
value: subscriptionUpdated.id
|
|
119
|
-
}
|
|
120
|
-
]
|
|
121
|
-
});
|
|
122
|
-
const subscriptionCanceled = subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end;
|
|
123
|
-
if (subscriptionCanceled) {
|
|
124
|
-
await options.subscription.onSubscriptionCancel?.({
|
|
125
|
-
subscription,
|
|
126
|
-
cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
|
|
127
|
-
stripeSubscription: subscriptionUpdated,
|
|
128
|
-
event
|
|
94
|
+
const subscriptionUpdated = event.data.object;
|
|
95
|
+
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
96
|
+
const plan = await getPlanByPriceId(options, priceId);
|
|
97
|
+
if (plan) {
|
|
98
|
+
const stripeId = subscriptionUpdated.id;
|
|
99
|
+
const subscription = await ctx.context.adapter.findOne({
|
|
100
|
+
model: "subscription",
|
|
101
|
+
where: [
|
|
102
|
+
{
|
|
103
|
+
field: "stripeSubscriptionId",
|
|
104
|
+
value: stripeId
|
|
105
|
+
}
|
|
106
|
+
]
|
|
129
107
|
});
|
|
108
|
+
if (!subscription) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const seats = subscriptionUpdated.items.data[0].quantity;
|
|
112
|
+
await ctx.context.adapter.update({
|
|
113
|
+
model: "subscription",
|
|
114
|
+
update: {
|
|
115
|
+
plan: plan.name.toLowerCase(),
|
|
116
|
+
limits: plan.limits,
|
|
117
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
118
|
+
status: subscriptionUpdated.status,
|
|
119
|
+
periodStart: new Date(
|
|
120
|
+
subscriptionUpdated.current_period_start * 1e3
|
|
121
|
+
),
|
|
122
|
+
periodEnd: new Date(subscriptionUpdated.current_period_end * 1e3),
|
|
123
|
+
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
124
|
+
seats
|
|
125
|
+
},
|
|
126
|
+
where: [
|
|
127
|
+
{
|
|
128
|
+
field: "stripeSubscriptionId",
|
|
129
|
+
value: subscriptionUpdated.id
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
});
|
|
133
|
+
const subscriptionCanceled = subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end;
|
|
134
|
+
if (subscriptionCanceled) {
|
|
135
|
+
await options.subscription.onSubscriptionCancel?.({
|
|
136
|
+
subscription,
|
|
137
|
+
cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
|
|
138
|
+
stripeSubscription: subscriptionUpdated,
|
|
139
|
+
event
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
await options.subscription.onSubscriptionUpdate?.({
|
|
143
|
+
event,
|
|
144
|
+
subscription
|
|
145
|
+
});
|
|
146
|
+
if (subscriptionUpdated.status === "active" && subscription.status === "trialing" && plan.freeTrial?.onTrialEnd) {
|
|
147
|
+
const user = await ctx.context.adapter.findOne({
|
|
148
|
+
model: "user",
|
|
149
|
+
where: [{ field: "id", value: subscription.referenceId }]
|
|
150
|
+
});
|
|
151
|
+
if (user) {
|
|
152
|
+
await plan.freeTrial.onTrialEnd({ subscription, user }, ctx.request);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) {
|
|
156
|
+
await plan.freeTrial.onTrialExpired(subscription, ctx.request);
|
|
157
|
+
}
|
|
130
158
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
subscription
|
|
134
|
-
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
betterAuth.logger.error(`Stripe webhook failed. Error: ${error.message}`);
|
|
135
161
|
}
|
|
136
162
|
}
|
|
137
163
|
async function onSubscriptionDeleted(ctx, options, event) {
|
|
138
164
|
if (!options.subscription?.enabled) {
|
|
139
165
|
return;
|
|
140
166
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (stripeSubscription.status === "canceled") {
|
|
147
|
-
const subscription = await ctx.context.adapter.findOne({
|
|
148
|
-
model: "subscription",
|
|
149
|
-
where: [
|
|
150
|
-
{
|
|
151
|
-
field: "id",
|
|
152
|
-
value: subscriptionId
|
|
153
|
-
}
|
|
154
|
-
]
|
|
155
|
-
});
|
|
156
|
-
if (subscription) {
|
|
157
|
-
await ctx.context.adapter.update({
|
|
167
|
+
try {
|
|
168
|
+
const subscriptionDeleted = event.data.object;
|
|
169
|
+
const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
|
|
170
|
+
if (subscriptionDeleted.status === "canceled") {
|
|
171
|
+
const subscription = await ctx.context.adapter.findOne({
|
|
158
172
|
model: "subscription",
|
|
159
173
|
where: [
|
|
160
174
|
{
|
|
161
175
|
field: "id",
|
|
162
|
-
value:
|
|
176
|
+
value: subscriptionId
|
|
163
177
|
}
|
|
164
|
-
]
|
|
165
|
-
update: {
|
|
166
|
-
status: "canceled"
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
await options.subscription.onSubscriptionDeleted?.({
|
|
170
|
-
event,
|
|
171
|
-
stripeSubscription: subscriptionDeleted,
|
|
172
|
-
subscription
|
|
178
|
+
]
|
|
173
179
|
});
|
|
180
|
+
if (subscription) {
|
|
181
|
+
await ctx.context.adapter.update({
|
|
182
|
+
model: "subscription",
|
|
183
|
+
where: [
|
|
184
|
+
{
|
|
185
|
+
field: "id",
|
|
186
|
+
value: subscription.id
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
update: {
|
|
190
|
+
status: "canceled"
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
await options.subscription.onSubscriptionDeleted?.({
|
|
194
|
+
event,
|
|
195
|
+
stripeSubscription: subscriptionDeleted,
|
|
196
|
+
subscription
|
|
197
|
+
});
|
|
198
|
+
}
|
|
174
199
|
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
betterAuth.logger.error(`Stripe webhook failed. Error: ${error.message}`);
|
|
175
202
|
}
|
|
176
203
|
}
|
|
177
204
|
|
package/dist/index.d.cts
CHANGED
|
@@ -78,7 +78,7 @@ type Plan = {
|
|
|
78
78
|
* @param subscription - Subscription
|
|
79
79
|
* @returns
|
|
80
80
|
*/
|
|
81
|
-
onTrialExpired?: (subscription: Subscription) => Promise<void>;
|
|
81
|
+
onTrialExpired?: (subscription: Subscription, request?: Request) => Promise<void>;
|
|
82
82
|
};
|
|
83
83
|
};
|
|
84
84
|
interface Subscription {
|
package/dist/index.d.mts
CHANGED
|
@@ -78,7 +78,7 @@ type Plan = {
|
|
|
78
78
|
* @param subscription - Subscription
|
|
79
79
|
* @returns
|
|
80
80
|
*/
|
|
81
|
-
onTrialExpired?: (subscription: Subscription) => Promise<void>;
|
|
81
|
+
onTrialExpired?: (subscription: Subscription, request?: Request) => Promise<void>;
|
|
82
82
|
};
|
|
83
83
|
};
|
|
84
84
|
interface Subscription {
|
package/dist/index.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ type Plan = {
|
|
|
78
78
|
* @param subscription - Subscription
|
|
79
79
|
* @returns
|
|
80
80
|
*/
|
|
81
|
-
onTrialExpired?: (subscription: Subscription) => Promise<void>;
|
|
81
|
+
onTrialExpired?: (subscription: Subscription, request?: Request) => Promise<void>;
|
|
82
82
|
};
|
|
83
83
|
};
|
|
84
84
|
interface Subscription {
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { getSessionFromCtx, sessionMiddleware, originCheck, APIError } from 'better-auth/api';
|
|
4
4
|
import { generateRandomString } from 'better-auth/crypto';
|
|
5
|
+
import { logger } from 'better-auth';
|
|
5
6
|
|
|
6
7
|
async function getPlans(options) {
|
|
7
8
|
return typeof options?.subscription?.plans === "function" ? await options.subscription?.plans() : options.subscription?.plans;
|
|
@@ -18,46 +19,37 @@ async function getPlanByName(options, name) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
update: {
|
|
43
|
-
plan: plan.name.toLowerCase(),
|
|
44
|
-
status: subscription.status,
|
|
45
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
46
|
-
periodStart: new Date(subscription.current_period_start * 1e3),
|
|
47
|
-
periodEnd: new Date(subscription.current_period_end * 1e3),
|
|
48
|
-
seats,
|
|
49
|
-
...trial
|
|
50
|
-
},
|
|
51
|
-
where: [
|
|
52
|
-
{
|
|
53
|
-
field: "id",
|
|
54
|
-
value: subscriptionId
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
});
|
|
58
|
-
if (!dbSubscription) {
|
|
59
|
-
dbSubscription = await ctx.context.adapter.findOne({
|
|
22
|
+
try {
|
|
23
|
+
const client = options.stripeClient;
|
|
24
|
+
const checkoutSession = event.data.object;
|
|
25
|
+
if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const subscription = await client.subscriptions.retrieve(
|
|
29
|
+
checkoutSession.subscription
|
|
30
|
+
);
|
|
31
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
32
|
+
const plan = await getPlanByPriceId(options, priceId);
|
|
33
|
+
if (plan) {
|
|
34
|
+
const referenceId = checkoutSession?.metadata?.referenceId;
|
|
35
|
+
const subscriptionId = checkoutSession?.metadata?.subscriptionId;
|
|
36
|
+
const seats = subscription.items.data[0].quantity;
|
|
37
|
+
if (referenceId && subscriptionId) {
|
|
38
|
+
const trial = subscription.trial_start && subscription.trial_end ? {
|
|
39
|
+
trialStart: new Date(subscription.trial_start * 1e3),
|
|
40
|
+
trialEnd: new Date(subscription.trial_end * 1e3)
|
|
41
|
+
} : {};
|
|
42
|
+
let dbSubscription = await ctx.context.adapter.update({
|
|
60
43
|
model: "subscription",
|
|
44
|
+
update: {
|
|
45
|
+
plan: plan.name.toLowerCase(),
|
|
46
|
+
status: subscription.status,
|
|
47
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
48
|
+
periodStart: new Date(subscription.current_period_start * 1e3),
|
|
49
|
+
periodEnd: new Date(subscription.current_period_end * 1e3),
|
|
50
|
+
seats,
|
|
51
|
+
...trial
|
|
52
|
+
},
|
|
61
53
|
where: [
|
|
62
54
|
{
|
|
63
55
|
field: "id",
|
|
@@ -65,111 +57,146 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
65
57
|
}
|
|
66
58
|
]
|
|
67
59
|
});
|
|
60
|
+
if (trial.trialStart && plan.freeTrial?.onTrialStart) {
|
|
61
|
+
await plan.freeTrial.onTrialStart(dbSubscription);
|
|
62
|
+
}
|
|
63
|
+
if (!dbSubscription) {
|
|
64
|
+
dbSubscription = await ctx.context.adapter.findOne({
|
|
65
|
+
model: "subscription",
|
|
66
|
+
where: [
|
|
67
|
+
{
|
|
68
|
+
field: "id",
|
|
69
|
+
value: subscriptionId
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
await options.subscription?.onSubscriptionComplete?.({
|
|
75
|
+
event,
|
|
76
|
+
subscription: dbSubscription,
|
|
77
|
+
stripeSubscription: subscription,
|
|
78
|
+
plan
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
68
81
|
}
|
|
69
|
-
await options.subscription?.onSubscriptionComplete?.({
|
|
70
|
-
event,
|
|
71
|
-
subscription: dbSubscription,
|
|
72
|
-
stripeSubscription: subscription,
|
|
73
|
-
plan
|
|
74
|
-
});
|
|
75
|
-
return;
|
|
76
82
|
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
async function onSubscriptionUpdated(ctx, options, event) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
const subscriptionUpdated = event.data.object;
|
|
84
|
-
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
85
|
-
const plan = await getPlanByPriceId(options, priceId);
|
|
86
|
-
if (plan) {
|
|
87
|
-
const stripeId = subscriptionUpdated.customer.toString();
|
|
88
|
-
const subscription = await ctx.context.adapter.findOne({
|
|
89
|
-
model: "subscription",
|
|
90
|
-
where: [
|
|
91
|
-
{
|
|
92
|
-
field: "stripeSubscriptionId",
|
|
93
|
-
value: stripeId
|
|
94
|
-
}
|
|
95
|
-
]
|
|
96
|
-
});
|
|
97
|
-
if (!subscription) {
|
|
88
|
+
try {
|
|
89
|
+
if (!options.subscription?.enabled) {
|
|
98
90
|
return;
|
|
99
91
|
}
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
where: [
|
|
114
|
-
{
|
|
115
|
-
field: "stripeSubscriptionId",
|
|
116
|
-
value: subscriptionUpdated.id
|
|
117
|
-
}
|
|
118
|
-
]
|
|
119
|
-
});
|
|
120
|
-
const subscriptionCanceled = subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end;
|
|
121
|
-
if (subscriptionCanceled) {
|
|
122
|
-
await options.subscription.onSubscriptionCancel?.({
|
|
123
|
-
subscription,
|
|
124
|
-
cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
|
|
125
|
-
stripeSubscription: subscriptionUpdated,
|
|
126
|
-
event
|
|
92
|
+
const subscriptionUpdated = event.data.object;
|
|
93
|
+
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
94
|
+
const plan = await getPlanByPriceId(options, priceId);
|
|
95
|
+
if (plan) {
|
|
96
|
+
const stripeId = subscriptionUpdated.id;
|
|
97
|
+
const subscription = await ctx.context.adapter.findOne({
|
|
98
|
+
model: "subscription",
|
|
99
|
+
where: [
|
|
100
|
+
{
|
|
101
|
+
field: "stripeSubscriptionId",
|
|
102
|
+
value: stripeId
|
|
103
|
+
}
|
|
104
|
+
]
|
|
127
105
|
});
|
|
106
|
+
if (!subscription) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const seats = subscriptionUpdated.items.data[0].quantity;
|
|
110
|
+
await ctx.context.adapter.update({
|
|
111
|
+
model: "subscription",
|
|
112
|
+
update: {
|
|
113
|
+
plan: plan.name.toLowerCase(),
|
|
114
|
+
limits: plan.limits,
|
|
115
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
116
|
+
status: subscriptionUpdated.status,
|
|
117
|
+
periodStart: new Date(
|
|
118
|
+
subscriptionUpdated.current_period_start * 1e3
|
|
119
|
+
),
|
|
120
|
+
periodEnd: new Date(subscriptionUpdated.current_period_end * 1e3),
|
|
121
|
+
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
122
|
+
seats
|
|
123
|
+
},
|
|
124
|
+
where: [
|
|
125
|
+
{
|
|
126
|
+
field: "stripeSubscriptionId",
|
|
127
|
+
value: subscriptionUpdated.id
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
const subscriptionCanceled = subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end;
|
|
132
|
+
if (subscriptionCanceled) {
|
|
133
|
+
await options.subscription.onSubscriptionCancel?.({
|
|
134
|
+
subscription,
|
|
135
|
+
cancellationDetails: subscriptionUpdated.cancellation_details || void 0,
|
|
136
|
+
stripeSubscription: subscriptionUpdated,
|
|
137
|
+
event
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
await options.subscription.onSubscriptionUpdate?.({
|
|
141
|
+
event,
|
|
142
|
+
subscription
|
|
143
|
+
});
|
|
144
|
+
if (subscriptionUpdated.status === "active" && subscription.status === "trialing" && plan.freeTrial?.onTrialEnd) {
|
|
145
|
+
const user = await ctx.context.adapter.findOne({
|
|
146
|
+
model: "user",
|
|
147
|
+
where: [{ field: "id", value: subscription.referenceId }]
|
|
148
|
+
});
|
|
149
|
+
if (user) {
|
|
150
|
+
await plan.freeTrial.onTrialEnd({ subscription, user }, ctx.request);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) {
|
|
154
|
+
await plan.freeTrial.onTrialExpired(subscription, ctx.request);
|
|
155
|
+
}
|
|
128
156
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
subscription
|
|
132
|
-
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error(`Stripe webhook failed. Error: ${error.message}`);
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
async function onSubscriptionDeleted(ctx, options, event) {
|
|
136
162
|
if (!options.subscription?.enabled) {
|
|
137
163
|
return;
|
|
138
164
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (stripeSubscription.status === "canceled") {
|
|
145
|
-
const subscription = await ctx.context.adapter.findOne({
|
|
146
|
-
model: "subscription",
|
|
147
|
-
where: [
|
|
148
|
-
{
|
|
149
|
-
field: "id",
|
|
150
|
-
value: subscriptionId
|
|
151
|
-
}
|
|
152
|
-
]
|
|
153
|
-
});
|
|
154
|
-
if (subscription) {
|
|
155
|
-
await ctx.context.adapter.update({
|
|
165
|
+
try {
|
|
166
|
+
const subscriptionDeleted = event.data.object;
|
|
167
|
+
const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
|
|
168
|
+
if (subscriptionDeleted.status === "canceled") {
|
|
169
|
+
const subscription = await ctx.context.adapter.findOne({
|
|
156
170
|
model: "subscription",
|
|
157
171
|
where: [
|
|
158
172
|
{
|
|
159
173
|
field: "id",
|
|
160
|
-
value:
|
|
174
|
+
value: subscriptionId
|
|
161
175
|
}
|
|
162
|
-
]
|
|
163
|
-
update: {
|
|
164
|
-
status: "canceled"
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
await options.subscription.onSubscriptionDeleted?.({
|
|
168
|
-
event,
|
|
169
|
-
stripeSubscription: subscriptionDeleted,
|
|
170
|
-
subscription
|
|
176
|
+
]
|
|
171
177
|
});
|
|
178
|
+
if (subscription) {
|
|
179
|
+
await ctx.context.adapter.update({
|
|
180
|
+
model: "subscription",
|
|
181
|
+
where: [
|
|
182
|
+
{
|
|
183
|
+
field: "id",
|
|
184
|
+
value: subscription.id
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
update: {
|
|
188
|
+
status: "canceled"
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
await options.subscription.onSubscriptionDeleted?.({
|
|
192
|
+
event,
|
|
193
|
+
stripeSubscription: subscriptionDeleted,
|
|
194
|
+
subscription
|
|
195
|
+
});
|
|
196
|
+
}
|
|
172
197
|
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.error(`Stripe webhook failed. Error: ${error.message}`);
|
|
173
200
|
}
|
|
174
201
|
}
|
|
175
202
|
|
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.1-beta.
|
|
4
|
+
"version": "1.2.1-beta.4",
|
|
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.1-beta.
|
|
38
|
+
"better-auth": "^1.2.1-beta.4"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/better-sqlite3": "^7.6.12",
|
package/src/hooks.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { logger, type GenericEndpointContext, type User } from "better-auth";
|
|
2
2
|
import type Stripe from "stripe";
|
|
3
3
|
import type { InputSubscription, StripeOptions, Subscription } from "./types";
|
|
4
4
|
import { getPlanByPriceId } from "./utils";
|
|
@@ -8,65 +8,76 @@ export async function onCheckoutSessionCompleted(
|
|
|
8
8
|
options: StripeOptions,
|
|
9
9
|
event: Stripe.Event,
|
|
10
10
|
) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
field: "id",
|
|
47
|
-
value: subscriptionId,
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
});
|
|
51
|
-
if (!dbSubscription) {
|
|
52
|
-
dbSubscription = await ctx.context.adapter.findOne<Subscription>({
|
|
53
|
-
model: "subscription",
|
|
54
|
-
where: [
|
|
55
|
-
{
|
|
56
|
-
field: "id",
|
|
57
|
-
value: subscriptionId,
|
|
11
|
+
try {
|
|
12
|
+
const client = options.stripeClient;
|
|
13
|
+
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
|
14
|
+
if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const subscription = await client.subscriptions.retrieve(
|
|
18
|
+
checkoutSession.subscription as string,
|
|
19
|
+
);
|
|
20
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
21
|
+
const plan = await getPlanByPriceId(options, priceId as string);
|
|
22
|
+
if (plan) {
|
|
23
|
+
const referenceId = checkoutSession?.metadata?.referenceId;
|
|
24
|
+
const subscriptionId = checkoutSession?.metadata?.subscriptionId;
|
|
25
|
+
const seats = subscription.items.data[0].quantity;
|
|
26
|
+
if (referenceId && subscriptionId) {
|
|
27
|
+
const trial =
|
|
28
|
+
subscription.trial_start && subscription.trial_end
|
|
29
|
+
? {
|
|
30
|
+
trialStart: new Date(subscription.trial_start * 1000),
|
|
31
|
+
trialEnd: new Date(subscription.trial_end * 1000),
|
|
32
|
+
}
|
|
33
|
+
: {};
|
|
34
|
+
|
|
35
|
+
let dbSubscription =
|
|
36
|
+
await ctx.context.adapter.update<InputSubscription>({
|
|
37
|
+
model: "subscription",
|
|
38
|
+
update: {
|
|
39
|
+
plan: plan.name.toLowerCase(),
|
|
40
|
+
status: subscription.status,
|
|
41
|
+
updatedAt: new Date(),
|
|
42
|
+
periodStart: new Date(subscription.current_period_start * 1000),
|
|
43
|
+
periodEnd: new Date(subscription.current_period_end * 1000),
|
|
44
|
+
seats,
|
|
45
|
+
...trial,
|
|
58
46
|
},
|
|
59
|
-
|
|
47
|
+
where: [
|
|
48
|
+
{
|
|
49
|
+
field: "id",
|
|
50
|
+
value: subscriptionId,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (trial.trialStart && plan.freeTrial?.onTrialStart) {
|
|
56
|
+
await plan.freeTrial.onTrialStart(dbSubscription as Subscription);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!dbSubscription) {
|
|
60
|
+
dbSubscription = await ctx.context.adapter.findOne<Subscription>({
|
|
61
|
+
model: "subscription",
|
|
62
|
+
where: [
|
|
63
|
+
{
|
|
64
|
+
field: "id",
|
|
65
|
+
value: subscriptionId,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
await options.subscription?.onSubscriptionComplete?.({
|
|
71
|
+
event,
|
|
72
|
+
subscription: dbSubscription as Subscription,
|
|
73
|
+
stripeSubscription: subscription,
|
|
74
|
+
plan,
|
|
60
75
|
});
|
|
76
|
+
return;
|
|
61
77
|
}
|
|
62
|
-
await options.subscription?.onSubscriptionComplete?.({
|
|
63
|
-
event,
|
|
64
|
-
subscription: dbSubscription as Subscription,
|
|
65
|
-
stripeSubscription: subscription,
|
|
66
|
-
plan,
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
69
78
|
}
|
|
79
|
+
} catch (e: any) {
|
|
80
|
+
logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
70
81
|
}
|
|
71
82
|
}
|
|
72
83
|
|
|
@@ -75,62 +86,89 @@ export async function onSubscriptionUpdated(
|
|
|
75
86
|
options: StripeOptions,
|
|
76
87
|
event: Stripe.Event,
|
|
77
88
|
) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
const subscriptionUpdated = event.data.object as Stripe.Subscription;
|
|
82
|
-
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
83
|
-
const plan = await getPlanByPriceId(options, priceId);
|
|
84
|
-
if (plan) {
|
|
85
|
-
const stripeId = subscriptionUpdated.customer.toString();
|
|
86
|
-
const subscription = await ctx.context.adapter.findOne<Subscription>({
|
|
87
|
-
model: "subscription",
|
|
88
|
-
where: [
|
|
89
|
-
{
|
|
90
|
-
field: "stripeSubscriptionId",
|
|
91
|
-
value: stripeId,
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
});
|
|
95
|
-
if (!subscription) {
|
|
89
|
+
try {
|
|
90
|
+
if (!options.subscription?.enabled) {
|
|
96
91
|
return;
|
|
97
92
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
93
|
+
const subscriptionUpdated = event.data.object as Stripe.Subscription;
|
|
94
|
+
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
95
|
+
const plan = await getPlanByPriceId(options, priceId);
|
|
96
|
+
if (plan) {
|
|
97
|
+
const stripeId = subscriptionUpdated.id;
|
|
98
|
+
const subscription = await ctx.context.adapter.findOne<Subscription>({
|
|
99
|
+
model: "subscription",
|
|
100
|
+
where: [
|
|
101
|
+
{
|
|
102
|
+
field: "stripeSubscriptionId",
|
|
103
|
+
value: stripeId,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
if (!subscription) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const seats = subscriptionUpdated.items.data[0].quantity;
|
|
111
|
+
await ctx.context.adapter.update({
|
|
112
|
+
model: "subscription",
|
|
113
|
+
update: {
|
|
114
|
+
plan: plan.name.toLowerCase(),
|
|
115
|
+
limits: plan.limits,
|
|
116
|
+
updatedAt: new Date(),
|
|
117
|
+
status: subscriptionUpdated.status,
|
|
118
|
+
periodStart: new Date(
|
|
119
|
+
subscriptionUpdated.current_period_start * 1000,
|
|
120
|
+
),
|
|
121
|
+
periodEnd: new Date(subscriptionUpdated.current_period_end * 1000),
|
|
122
|
+
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
123
|
+
seats,
|
|
115
124
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
where: [
|
|
126
|
+
{
|
|
127
|
+
field: "stripeSubscriptionId",
|
|
128
|
+
value: subscriptionUpdated.id,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
const subscriptionCanceled =
|
|
133
|
+
subscriptionUpdated.status === "active" &&
|
|
134
|
+
subscriptionUpdated.cancel_at_period_end;
|
|
135
|
+
if (subscriptionCanceled) {
|
|
136
|
+
await options.subscription.onSubscriptionCancel?.({
|
|
137
|
+
subscription,
|
|
138
|
+
cancellationDetails:
|
|
139
|
+
subscriptionUpdated.cancellation_details || undefined,
|
|
140
|
+
stripeSubscription: subscriptionUpdated,
|
|
141
|
+
event,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
await options.subscription.onSubscriptionUpdate?.({
|
|
127
145
|
event,
|
|
146
|
+
subscription,
|
|
128
147
|
});
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
subscriptionUpdated.status === "active" &&
|
|
151
|
+
subscription.status === "trialing" &&
|
|
152
|
+
plan.freeTrial?.onTrialEnd
|
|
153
|
+
) {
|
|
154
|
+
const user = await ctx.context.adapter.findOne<User>({
|
|
155
|
+
model: "user",
|
|
156
|
+
where: [{ field: "id", value: subscription.referenceId }],
|
|
157
|
+
});
|
|
158
|
+
if (user) {
|
|
159
|
+
await plan.freeTrial.onTrialEnd({ subscription, user }, ctx.request);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
subscriptionUpdated.status === "incomplete_expired" &&
|
|
164
|
+
subscription.status === "trialing" &&
|
|
165
|
+
plan.freeTrial?.onTrialExpired
|
|
166
|
+
) {
|
|
167
|
+
await plan.freeTrial.onTrialExpired(subscription, ctx.request);
|
|
168
|
+
}
|
|
129
169
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
subscription,
|
|
133
|
-
});
|
|
170
|
+
} catch (error: any) {
|
|
171
|
+
logger.error(`Stripe webhook failed. Error: ${error.message}`);
|
|
134
172
|
}
|
|
135
173
|
}
|
|
136
174
|
|
|
@@ -142,39 +180,40 @@ export async function onSubscriptionDeleted(
|
|
|
142
180
|
if (!options.subscription?.enabled) {
|
|
143
181
|
return;
|
|
144
182
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (stripeSubscription.status === "canceled") {
|
|
151
|
-
const subscription = await ctx.context.adapter.findOne<Subscription>({
|
|
152
|
-
model: "subscription",
|
|
153
|
-
where: [
|
|
154
|
-
{
|
|
155
|
-
field: "id",
|
|
156
|
-
value: subscriptionId,
|
|
157
|
-
},
|
|
158
|
-
],
|
|
159
|
-
});
|
|
160
|
-
if (subscription) {
|
|
161
|
-
await ctx.context.adapter.update({
|
|
183
|
+
try {
|
|
184
|
+
const subscriptionDeleted = event.data.object as Stripe.Subscription;
|
|
185
|
+
const subscriptionId = subscriptionDeleted.metadata?.subscriptionId;
|
|
186
|
+
if (subscriptionDeleted.status === "canceled") {
|
|
187
|
+
const subscription = await ctx.context.adapter.findOne<Subscription>({
|
|
162
188
|
model: "subscription",
|
|
163
189
|
where: [
|
|
164
190
|
{
|
|
165
191
|
field: "id",
|
|
166
|
-
value:
|
|
192
|
+
value: subscriptionId,
|
|
167
193
|
},
|
|
168
194
|
],
|
|
169
|
-
update: {
|
|
170
|
-
status: "canceled",
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
await options.subscription.onSubscriptionDeleted?.({
|
|
174
|
-
event,
|
|
175
|
-
stripeSubscription: subscriptionDeleted,
|
|
176
|
-
subscription,
|
|
177
195
|
});
|
|
196
|
+
if (subscription) {
|
|
197
|
+
await ctx.context.adapter.update({
|
|
198
|
+
model: "subscription",
|
|
199
|
+
where: [
|
|
200
|
+
{
|
|
201
|
+
field: "id",
|
|
202
|
+
value: subscription.id,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
update: {
|
|
206
|
+
status: "canceled",
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
await options.subscription.onSubscriptionDeleted?.({
|
|
210
|
+
event,
|
|
211
|
+
stripeSubscription: subscriptionDeleted,
|
|
212
|
+
subscription,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
178
215
|
}
|
|
216
|
+
} catch (error: any) {
|
|
217
|
+
logger.error(`Stripe webhook failed. Error: ${error.message}`);
|
|
179
218
|
}
|
|
180
219
|
}
|
package/src/stripe.test.ts
CHANGED
|
@@ -453,4 +453,232 @@ describe("stripe", async () => {
|
|
|
453
453
|
expect(updatedSubscription?.status).toBe("canceled");
|
|
454
454
|
}
|
|
455
455
|
});
|
|
456
|
+
|
|
457
|
+
it("should execute subscription event handlers", async () => {
|
|
458
|
+
const onSubscriptionComplete = vi.fn();
|
|
459
|
+
const onSubscriptionUpdate = vi.fn();
|
|
460
|
+
const onSubscriptionCancel = vi.fn();
|
|
461
|
+
const onSubscriptionDeleted = vi.fn();
|
|
462
|
+
|
|
463
|
+
const testOptions = {
|
|
464
|
+
...stripeOptions,
|
|
465
|
+
subscription: {
|
|
466
|
+
...stripeOptions.subscription,
|
|
467
|
+
onSubscriptionComplete,
|
|
468
|
+
onSubscriptionUpdate,
|
|
469
|
+
onSubscriptionCancel,
|
|
470
|
+
onSubscriptionDeleted,
|
|
471
|
+
},
|
|
472
|
+
stripeWebhookSecret: "test_secret",
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const testAuth = betterAuth({
|
|
476
|
+
baseURL: "http://localhost:3000",
|
|
477
|
+
database: memory,
|
|
478
|
+
emailAndPassword: {
|
|
479
|
+
enabled: true,
|
|
480
|
+
},
|
|
481
|
+
plugins: [stripe(testOptions)],
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Test subscription complete handler
|
|
485
|
+
const completeEvent = {
|
|
486
|
+
type: "checkout.session.completed",
|
|
487
|
+
data: {
|
|
488
|
+
object: {
|
|
489
|
+
mode: "subscription",
|
|
490
|
+
subscription: "sub_123",
|
|
491
|
+
metadata: {
|
|
492
|
+
referenceId: "user_123",
|
|
493
|
+
subscriptionId: "sub_123",
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const mockSubscription = {
|
|
500
|
+
id: "sub_123",
|
|
501
|
+
status: "active",
|
|
502
|
+
items: {
|
|
503
|
+
data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
|
|
504
|
+
},
|
|
505
|
+
current_period_start: Math.floor(Date.now() / 1000),
|
|
506
|
+
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const mockStripeForEvents = {
|
|
510
|
+
...testOptions.stripeClient,
|
|
511
|
+
subscriptions: {
|
|
512
|
+
retrieve: vi.fn().mockResolvedValue(mockSubscription),
|
|
513
|
+
},
|
|
514
|
+
webhooks: {
|
|
515
|
+
constructEvent: vi.fn().mockReturnValue(completeEvent),
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const eventTestOptions = {
|
|
520
|
+
...testOptions,
|
|
521
|
+
stripeClient: mockStripeForEvents as unknown as Stripe,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const eventTestAuth = betterAuth({
|
|
525
|
+
baseURL: "http://localhost:3000",
|
|
526
|
+
database: memory,
|
|
527
|
+
emailAndPassword: { enabled: true },
|
|
528
|
+
plugins: [stripe(eventTestOptions)],
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
await ctx.adapter.create({
|
|
532
|
+
model: "subscription",
|
|
533
|
+
data: {
|
|
534
|
+
id: "sub_123",
|
|
535
|
+
referenceId: "user_123",
|
|
536
|
+
stripeCustomerId: "cus_123",
|
|
537
|
+
stripeSubscriptionId: "sub_123",
|
|
538
|
+
status: "incomplete",
|
|
539
|
+
plan: "starter",
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const webhookRequest = new Request(
|
|
544
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
545
|
+
{
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: {
|
|
548
|
+
"stripe-signature": "test_signature",
|
|
549
|
+
},
|
|
550
|
+
body: JSON.stringify(completeEvent),
|
|
551
|
+
},
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
await eventTestAuth.handler(webhookRequest);
|
|
555
|
+
|
|
556
|
+
expect(onSubscriptionComplete).toHaveBeenCalledWith(
|
|
557
|
+
expect.objectContaining({
|
|
558
|
+
event: expect.any(Object),
|
|
559
|
+
subscription: expect.any(Object),
|
|
560
|
+
stripeSubscription: expect.any(Object),
|
|
561
|
+
plan: expect.any(Object),
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const updateEvent = {
|
|
566
|
+
type: "customer.subscription.updated",
|
|
567
|
+
data: {
|
|
568
|
+
object: {
|
|
569
|
+
id: "sub_123",
|
|
570
|
+
customer: "cus_123",
|
|
571
|
+
status: "active",
|
|
572
|
+
items: {
|
|
573
|
+
data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
|
|
574
|
+
},
|
|
575
|
+
current_period_start: Math.floor(Date.now() / 1000),
|
|
576
|
+
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const updateRequest = new Request(
|
|
582
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
583
|
+
{
|
|
584
|
+
method: "POST",
|
|
585
|
+
headers: {
|
|
586
|
+
"stripe-signature": "test_signature",
|
|
587
|
+
},
|
|
588
|
+
body: JSON.stringify(updateEvent),
|
|
589
|
+
},
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
mockStripeForEvents.webhooks.constructEvent.mockReturnValue(updateEvent);
|
|
593
|
+
await eventTestAuth.handler(updateRequest);
|
|
594
|
+
|
|
595
|
+
expect(onSubscriptionUpdate).toHaveBeenCalledWith(
|
|
596
|
+
expect.objectContaining({
|
|
597
|
+
event: expect.any(Object),
|
|
598
|
+
subscription: expect.any(Object),
|
|
599
|
+
}),
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
const cancelEvent = {
|
|
603
|
+
type: "customer.subscription.updated",
|
|
604
|
+
data: {
|
|
605
|
+
object: {
|
|
606
|
+
id: "sub_123",
|
|
607
|
+
customer: "cus_123",
|
|
608
|
+
status: "active",
|
|
609
|
+
cancel_at_period_end: true,
|
|
610
|
+
items: {
|
|
611
|
+
data: [{ price: { id: process.env.STRIPE_PRICE_ID_1 } }],
|
|
612
|
+
},
|
|
613
|
+
current_period_start: Math.floor(Date.now() / 1000),
|
|
614
|
+
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const cancelRequest = new Request(
|
|
620
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
621
|
+
{
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers: {
|
|
624
|
+
"stripe-signature": "test_signature",
|
|
625
|
+
},
|
|
626
|
+
body: JSON.stringify(cancelEvent),
|
|
627
|
+
},
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
mockStripeForEvents.webhooks.constructEvent.mockReturnValue(cancelEvent);
|
|
631
|
+
await eventTestAuth.handler(cancelRequest);
|
|
632
|
+
|
|
633
|
+
expect(onSubscriptionCancel).toHaveBeenCalledWith({
|
|
634
|
+
event: cancelEvent,
|
|
635
|
+
subscription: expect.objectContaining({
|
|
636
|
+
id: "sub_123",
|
|
637
|
+
}),
|
|
638
|
+
stripeSubscription: expect.objectContaining({
|
|
639
|
+
id: "sub_123",
|
|
640
|
+
cancel_at_period_end: true,
|
|
641
|
+
}),
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const deleteEvent = {
|
|
645
|
+
type: "customer.subscription.deleted",
|
|
646
|
+
data: {
|
|
647
|
+
object: {
|
|
648
|
+
id: "sub_123",
|
|
649
|
+
customer: "cus_123",
|
|
650
|
+
status: "canceled",
|
|
651
|
+
metadata: {
|
|
652
|
+
referenceId: "user_123",
|
|
653
|
+
subscriptionId: "sub_123",
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const deleteRequest = new Request(
|
|
660
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
661
|
+
{
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers: {
|
|
664
|
+
"stripe-signature": "test_signature",
|
|
665
|
+
},
|
|
666
|
+
body: JSON.stringify(deleteEvent),
|
|
667
|
+
},
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
mockStripeForEvents.webhooks.constructEvent.mockReturnValue(deleteEvent);
|
|
671
|
+
await eventTestAuth.handler(deleteRequest);
|
|
672
|
+
|
|
673
|
+
expect(onSubscriptionDeleted).toHaveBeenCalledWith({
|
|
674
|
+
event: deleteEvent,
|
|
675
|
+
subscription: expect.objectContaining({
|
|
676
|
+
id: "sub_123",
|
|
677
|
+
}),
|
|
678
|
+
stripeSubscription: expect.objectContaining({
|
|
679
|
+
id: "sub_123",
|
|
680
|
+
status: "canceled",
|
|
681
|
+
}),
|
|
682
|
+
});
|
|
683
|
+
});
|
|
456
684
|
});
|
package/src/types.ts
CHANGED
|
@@ -77,7 +77,10 @@ export type Plan = {
|
|
|
77
77
|
* @param subscription - Subscription
|
|
78
78
|
* @returns
|
|
79
79
|
*/
|
|
80
|
-
onTrialExpired?: (
|
|
80
|
+
onTrialExpired?: (
|
|
81
|
+
subscription: Subscription,
|
|
82
|
+
request?: Request,
|
|
83
|
+
) => Promise<void>;
|
|
81
84
|
};
|
|
82
85
|
};
|
|
83
86
|
|