@faire-auth/stripe 1.0.0
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 +24 -0
- package/.turbo/turbo-test.log +1578 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +22 -0
- package/LICENSE +661 -0
- package/dist/client.cjs +15 -0
- package/dist/client.d.cts +25 -0
- package/dist/client.d.mts +25 -0
- package/dist/client.mjs +14 -0
- package/dist/index-6UTHxaqg.d.cts +1110 -0
- package/dist/index-CYV4awaB.d.mts +1110 -0
- package/dist/index.cjs +992 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +968 -0
- package/overrides.d.ts +32 -0
- package/package.json +69 -0
- package/src/client.ts +35 -0
- package/src/hooks.ts +265 -0
- package/src/index.ts +1533 -0
- package/src/schema.ts +92 -0
- package/src/stripe.test.ts +1425 -0
- package/src/types.ts +355 -0
- package/src/utils.ts +70 -0
- package/tsconfig.json +17 -0
- package/tsdown.config.ts +16 -0
- package/vitest.config.ts +9 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
import { callbackURLSchema, createRoute, redirectUrlSchema, req, res } from "@faire-auth/core/factory";
|
|
2
|
+
import { False, True } from "@faire-auth/core/static";
|
|
3
|
+
import { logger } from "faire-auth";
|
|
4
|
+
import { getSessionFromCtx, originCheck, sessionMiddleware } from "faire-auth/middleware";
|
|
5
|
+
import { createEndpoint, createMiddleware } from "faire-auth/plugins";
|
|
6
|
+
import Stripe from "stripe";
|
|
7
|
+
import * as z from "zod";
|
|
8
|
+
import { mergeSchema } from "faire-auth/db";
|
|
9
|
+
|
|
10
|
+
//#region src/utils.ts
|
|
11
|
+
async function getPlans(subscriptionOptions) {
|
|
12
|
+
if (subscriptionOptions?.enabled) return typeof subscriptionOptions.plans === "function" ? await subscriptionOptions.plans() : subscriptionOptions.plans;
|
|
13
|
+
throw new Error("Subscriptions are not enabled in the Stripe options.");
|
|
14
|
+
}
|
|
15
|
+
async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
|
|
16
|
+
return await getPlans(options.subscription).then((res$1) => res$1?.find((plan) => plan.priceId === priceId || plan.annualDiscountPriceId === priceId || priceLookupKey && (plan.lookupKey === priceLookupKey || plan.annualDiscountLookupKey === priceLookupKey)));
|
|
17
|
+
}
|
|
18
|
+
async function getPlanByName(options, name) {
|
|
19
|
+
return await getPlans(options.subscription).then((res$1) => res$1?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/hooks.ts
|
|
24
|
+
async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
25
|
+
try {
|
|
26
|
+
const client = options.stripeClient;
|
|
27
|
+
const checkoutSession = event.data.object;
|
|
28
|
+
if (checkoutSession.mode === "setup" || !options.subscription?.enabled) return;
|
|
29
|
+
const subscription = await client.subscriptions.retrieve(checkoutSession.subscription);
|
|
30
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
31
|
+
const plan = await getPlanByPriceInfo(options, priceId, subscription.items.data[0]?.price.lookup_key || null);
|
|
32
|
+
if (plan) {
|
|
33
|
+
const referenceId = checkoutSession?.client_reference_id || checkoutSession?.metadata?.referenceId;
|
|
34
|
+
const subscriptionId = checkoutSession?.metadata?.subscriptionId;
|
|
35
|
+
const seats = subscription.items.data[0].quantity;
|
|
36
|
+
if (referenceId && subscriptionId) {
|
|
37
|
+
const trial = subscription.trial_start && subscription.trial_end ? {
|
|
38
|
+
trialStart: /* @__PURE__ */ new Date(subscription.trial_start * 1e3),
|
|
39
|
+
trialEnd: /* @__PURE__ */ new Date(subscription.trial_end * 1e3)
|
|
40
|
+
} : {};
|
|
41
|
+
let dbSubscription = await ctx.get("context").adapter.update({
|
|
42
|
+
model: "subscription",
|
|
43
|
+
update: {
|
|
44
|
+
plan: plan.name.toLowerCase(),
|
|
45
|
+
status: subscription.status,
|
|
46
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
47
|
+
periodStart: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_start * 1e3),
|
|
48
|
+
periodEnd: /* @__PURE__ */ new Date(subscription.items.data[0].current_period_end * 1e3),
|
|
49
|
+
stripeSubscriptionId: checkoutSession.subscription,
|
|
50
|
+
seats,
|
|
51
|
+
...trial
|
|
52
|
+
},
|
|
53
|
+
where: [{
|
|
54
|
+
field: "id",
|
|
55
|
+
value: subscriptionId
|
|
56
|
+
}]
|
|
57
|
+
});
|
|
58
|
+
if (trial.trialStart && plan.freeTrial?.onTrialStart) await plan.freeTrial.onTrialStart(dbSubscription);
|
|
59
|
+
if (!dbSubscription) dbSubscription = await ctx.get("context").adapter.findOne({
|
|
60
|
+
model: "subscription",
|
|
61
|
+
where: [{
|
|
62
|
+
field: "id",
|
|
63
|
+
value: subscriptionId
|
|
64
|
+
}]
|
|
65
|
+
});
|
|
66
|
+
await options.subscription?.onSubscriptionComplete?.({
|
|
67
|
+
event,
|
|
68
|
+
subscription: dbSubscription,
|
|
69
|
+
stripeSubscription: subscription,
|
|
70
|
+
plan
|
|
71
|
+
}, ctx);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function onSubscriptionUpdated(ctx, options, event) {
|
|
80
|
+
try {
|
|
81
|
+
if (!options.subscription?.enabled) return;
|
|
82
|
+
const subscriptionUpdated = event.data.object;
|
|
83
|
+
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
84
|
+
const plan = await getPlanByPriceInfo(options, priceId, subscriptionUpdated.items.data[0].price.lookup_key || null);
|
|
85
|
+
const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
|
|
86
|
+
const customerId = subscriptionUpdated.customer?.toString();
|
|
87
|
+
let subscription = await ctx.get("context").adapter.findOne({
|
|
88
|
+
model: "subscription",
|
|
89
|
+
where: subscriptionId ? [{
|
|
90
|
+
field: "id",
|
|
91
|
+
value: subscriptionId
|
|
92
|
+
}] : [{
|
|
93
|
+
field: "stripeSubscriptionId",
|
|
94
|
+
value: subscriptionUpdated.id
|
|
95
|
+
}]
|
|
96
|
+
});
|
|
97
|
+
if (!subscription) {
|
|
98
|
+
const subs = await ctx.get("context").adapter.findMany({
|
|
99
|
+
model: "subscription",
|
|
100
|
+
where: [{
|
|
101
|
+
field: "stripeCustomerId",
|
|
102
|
+
value: customerId
|
|
103
|
+
}]
|
|
104
|
+
});
|
|
105
|
+
if (subs.length > 1) {
|
|
106
|
+
const activeSub = subs.find((sub) => sub.status === "active" || sub.status === "trialing");
|
|
107
|
+
if (!activeSub) {
|
|
108
|
+
logger.warn(`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
subscription = activeSub;
|
|
112
|
+
} else subscription = subs[0];
|
|
113
|
+
}
|
|
114
|
+
const seats = subscriptionUpdated.items.data[0].quantity;
|
|
115
|
+
await ctx.get("context").adapter.update({
|
|
116
|
+
model: "subscription",
|
|
117
|
+
update: {
|
|
118
|
+
...plan ? {
|
|
119
|
+
plan: plan.name.toLowerCase(),
|
|
120
|
+
limits: plan.limits
|
|
121
|
+
} : {},
|
|
122
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
123
|
+
status: subscriptionUpdated.status,
|
|
124
|
+
periodStart: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_start * 1e3),
|
|
125
|
+
periodEnd: /* @__PURE__ */ new Date(subscriptionUpdated.items.data[0].current_period_end * 1e3),
|
|
126
|
+
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
127
|
+
seats,
|
|
128
|
+
stripeSubscriptionId: subscriptionUpdated.id
|
|
129
|
+
},
|
|
130
|
+
where: [{
|
|
131
|
+
field: "id",
|
|
132
|
+
value: subscription.id
|
|
133
|
+
}]
|
|
134
|
+
});
|
|
135
|
+
if (subscriptionUpdated.status === "active" && subscriptionUpdated.cancel_at_period_end && !subscription.cancelAtPeriodEnd) await options.subscription.onSubscriptionCancel?.({
|
|
136
|
+
subscription,
|
|
137
|
+
...subscriptionUpdated.cancellation_details !== void 0 && { cancellationDetails: subscriptionUpdated.cancellation_details },
|
|
138
|
+
stripeSubscription: subscriptionUpdated,
|
|
139
|
+
event
|
|
140
|
+
});
|
|
141
|
+
await options.subscription.onSubscriptionUpdate?.({
|
|
142
|
+
event,
|
|
143
|
+
subscription
|
|
144
|
+
});
|
|
145
|
+
if (plan) {
|
|
146
|
+
if (subscriptionUpdated.status === "active" && subscription.status === "trialing" && plan.freeTrial?.onTrialEnd) await plan.freeTrial.onTrialEnd({ subscription }, ctx);
|
|
147
|
+
if (subscriptionUpdated.status === "incomplete_expired" && subscription.status === "trialing" && plan.freeTrial?.onTrialExpired) await plan.freeTrial.onTrialExpired(subscription, ctx);
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function onSubscriptionDeleted(ctx, options, event) {
|
|
154
|
+
if (!options.subscription?.enabled) return;
|
|
155
|
+
try {
|
|
156
|
+
const subscriptionDeleted = event.data.object;
|
|
157
|
+
const subscriptionId = subscriptionDeleted.id;
|
|
158
|
+
const subscription = await ctx.get("context").adapter.findOne({
|
|
159
|
+
model: "subscription",
|
|
160
|
+
where: [{
|
|
161
|
+
field: "stripeSubscriptionId",
|
|
162
|
+
value: subscriptionId
|
|
163
|
+
}]
|
|
164
|
+
});
|
|
165
|
+
if (subscription) {
|
|
166
|
+
await ctx.get("context").adapter.update({
|
|
167
|
+
model: "subscription",
|
|
168
|
+
where: [{
|
|
169
|
+
field: "id",
|
|
170
|
+
value: subscription.id
|
|
171
|
+
}],
|
|
172
|
+
update: {
|
|
173
|
+
status: "canceled",
|
|
174
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
await options.subscription.onSubscriptionDeleted?.({
|
|
178
|
+
event,
|
|
179
|
+
stripeSubscription: subscriptionDeleted,
|
|
180
|
+
subscription
|
|
181
|
+
});
|
|
182
|
+
} else logger.warn(`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/schema.ts
|
|
190
|
+
const subscriptions = { subscription: { fields: {
|
|
191
|
+
plan: {
|
|
192
|
+
type: "string",
|
|
193
|
+
required: true
|
|
194
|
+
},
|
|
195
|
+
referenceId: {
|
|
196
|
+
type: "string",
|
|
197
|
+
required: true
|
|
198
|
+
},
|
|
199
|
+
stripeCustomerId: {
|
|
200
|
+
type: "string",
|
|
201
|
+
required: false
|
|
202
|
+
},
|
|
203
|
+
stripeSubscriptionId: {
|
|
204
|
+
type: "string",
|
|
205
|
+
required: false
|
|
206
|
+
},
|
|
207
|
+
status: {
|
|
208
|
+
type: "string",
|
|
209
|
+
defaultValue: "incomplete"
|
|
210
|
+
},
|
|
211
|
+
periodStart: {
|
|
212
|
+
type: "date",
|
|
213
|
+
required: false
|
|
214
|
+
},
|
|
215
|
+
periodEnd: {
|
|
216
|
+
type: "date",
|
|
217
|
+
required: false
|
|
218
|
+
},
|
|
219
|
+
trialStart: {
|
|
220
|
+
type: "date",
|
|
221
|
+
required: false
|
|
222
|
+
},
|
|
223
|
+
trialEnd: {
|
|
224
|
+
type: "date",
|
|
225
|
+
required: false
|
|
226
|
+
},
|
|
227
|
+
cancelAtPeriodEnd: {
|
|
228
|
+
type: "boolean",
|
|
229
|
+
required: false,
|
|
230
|
+
defaultValue: false
|
|
231
|
+
},
|
|
232
|
+
seats: {
|
|
233
|
+
type: "number",
|
|
234
|
+
required: false
|
|
235
|
+
}
|
|
236
|
+
} } };
|
|
237
|
+
const user = { user: { fields: { stripeCustomerId: {
|
|
238
|
+
type: "string",
|
|
239
|
+
required: false
|
|
240
|
+
} } } };
|
|
241
|
+
const getSchema = (options) => {
|
|
242
|
+
let baseSchema = {};
|
|
243
|
+
if (options.subscription?.enabled) baseSchema = {
|
|
244
|
+
...subscriptions,
|
|
245
|
+
...user
|
|
246
|
+
};
|
|
247
|
+
else baseSchema = { ...user };
|
|
248
|
+
if (options.schema && !options.subscription?.enabled && "subscription" in options.schema) {
|
|
249
|
+
const { subscription,...restSchema } = options.schema;
|
|
250
|
+
return mergeSchema(baseSchema, restSchema);
|
|
251
|
+
}
|
|
252
|
+
return mergeSchema(baseSchema, options.schema);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/index.ts
|
|
257
|
+
const STRIPE_ERROR_CODES = {
|
|
258
|
+
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
|
|
259
|
+
SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
|
|
260
|
+
ALREADY_SUBSCRIBED_PLAN: "You're already subscribed to this plan",
|
|
261
|
+
UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
|
|
262
|
+
FAILED_TO_FETCH_PLANS: "Failed to fetch plans",
|
|
263
|
+
EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
|
|
264
|
+
SUBSCRIPTION_NOT_ACTIVE: "Subscription is not active",
|
|
265
|
+
SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION: "Subscription is not scheduled for cancellation"
|
|
266
|
+
};
|
|
267
|
+
const getUrl = (options, url) => {
|
|
268
|
+
if (url.startsWith("http")) return url;
|
|
269
|
+
return `${options.baseURL}${url.startsWith("/") ? url : `/${url}`}`;
|
|
270
|
+
};
|
|
271
|
+
async function resolvePriceIdFromLookupKey(stripeClient, lookupKey) {
|
|
272
|
+
if (!lookupKey) return void 0;
|
|
273
|
+
return (await stripeClient.prices.list({
|
|
274
|
+
lookup_keys: [lookupKey],
|
|
275
|
+
active: true,
|
|
276
|
+
limit: 1
|
|
277
|
+
})).data[0]?.id;
|
|
278
|
+
}
|
|
279
|
+
const stripe = (options) => {
|
|
280
|
+
const client = options.stripeClient;
|
|
281
|
+
const referenceMiddleware = (action) => createMiddleware()(async (ctx, next) => {
|
|
282
|
+
const session = ctx.get("session");
|
|
283
|
+
if (!session) return ctx.json({ success: False }, 401);
|
|
284
|
+
if (!options.subscription?.enabled) {
|
|
285
|
+
logger.error(`Subscription actions are not enabled in your stripe plugin config.`);
|
|
286
|
+
return ctx.json({ success: False }, 400);
|
|
287
|
+
}
|
|
288
|
+
let body = {};
|
|
289
|
+
let bodyHasId = false;
|
|
290
|
+
try {
|
|
291
|
+
body = await ctx.req.raw.clone().json();
|
|
292
|
+
} catch {}
|
|
293
|
+
if (body.referenceId) bodyHasId = true;
|
|
294
|
+
else body.referenceId = ctx.req.query("referenceId") ?? session.user.id;
|
|
295
|
+
if (bodyHasId && !options.subscription?.authorizeReference) {
|
|
296
|
+
logger.error(`Passing referenceId into a subscription action isn't allowed if subscription.authorizeReference isn't defined in your stripe plugin config.`);
|
|
297
|
+
return ctx.json({
|
|
298
|
+
success: False,
|
|
299
|
+
message: "Reference id is not allowed. Read server logs for more details."
|
|
300
|
+
}, 400);
|
|
301
|
+
}
|
|
302
|
+
if (!(bodyHasId ? await options.subscription?.authorizeReference?.({
|
|
303
|
+
user: session.user,
|
|
304
|
+
session: session.session,
|
|
305
|
+
referenceId: body.referenceId,
|
|
306
|
+
action
|
|
307
|
+
}, ctx) : true)) return ctx.json({ success: False }, 401);
|
|
308
|
+
return await next();
|
|
309
|
+
});
|
|
310
|
+
const subscriptionEndpoints = {
|
|
311
|
+
upgradeSubscription: createEndpoint(createRoute({
|
|
312
|
+
operationId: "upgradeSubscription",
|
|
313
|
+
method: "post",
|
|
314
|
+
path: "/subscription/upgrade",
|
|
315
|
+
middleware: [
|
|
316
|
+
sessionMiddleware(),
|
|
317
|
+
originCheck(async (ctx) => {
|
|
318
|
+
const { successURL, cancelURL } = await ctx.req.raw.clone().json();
|
|
319
|
+
return [successURL, cancelURL];
|
|
320
|
+
}),
|
|
321
|
+
referenceMiddleware("upgrade-subscription")
|
|
322
|
+
],
|
|
323
|
+
request: req().bdy(z.object({
|
|
324
|
+
plan: z.string().meta({ description: "The name of the plan to upgrade to. Eg: \"pro\"" }),
|
|
325
|
+
annual: z.boolean().meta({ description: "Whether to upgrade to an annual plan. Eg: true" }).optional(),
|
|
326
|
+
referenceId: z.string().meta({ description: "Reference id of the subscription to upgrade. Eg: \"123\"" }).optional(),
|
|
327
|
+
subscriptionId: z.string().meta({ description: "The id of the subscription to upgrade. Eg: \"sub_123\"" }).optional(),
|
|
328
|
+
metadata: z.record(z.string(), z.any()).optional(),
|
|
329
|
+
seats: z.number().meta({ description: "Number of seats to upgrade to (if applicable). Eg: 1" }).optional(),
|
|
330
|
+
successUrl: z.string().meta({ description: "Callback URL to redirect back after successful subscription. Eg: \"https://example.com/success\"" }).default("/"),
|
|
331
|
+
cancelUrl: z.string().meta({ description: "If set, checkout shows a back button and customers will be directed here if they cancel payment. Eg: \"https://example.com/pricing\"" }).default("/"),
|
|
332
|
+
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: \"https://example.com/dashboard\"" }).optional(),
|
|
333
|
+
disableRedirect: z.boolean().meta({ description: "Disable redirect after successful subscription. Eg: true" }).default(false)
|
|
334
|
+
})).bld(),
|
|
335
|
+
responses: res(z.intersection(redirectUrlSchema, z.record(z.string(), z.any()))).err(400).err(401).err(500).bld()
|
|
336
|
+
}), (authOptions) => async (ctx) => {
|
|
337
|
+
if (!options.subscription?.enabled) {
|
|
338
|
+
logger.error(`Subscription actions are not enabled in your stripe plugin config.`);
|
|
339
|
+
return ctx.json({ success: False }, 400);
|
|
340
|
+
}
|
|
341
|
+
const { user: user$1, session } = ctx.get("session");
|
|
342
|
+
if (!user$1.emailVerified && options.subscription?.enabled && options.subscription.requireEmailVerification) return ctx.json({
|
|
343
|
+
success: False,
|
|
344
|
+
message: STRIPE_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED
|
|
345
|
+
}, 400);
|
|
346
|
+
const { referenceId = user$1.id, plan: requestPlan, subscriptionId, metadata, seats, returnUrl, cancelUrl, disableRedirect, successUrl, annual } = ctx.req.valid("json");
|
|
347
|
+
const plan = await getPlanByName(options, requestPlan);
|
|
348
|
+
if (!plan) return ctx.json({
|
|
349
|
+
success: False,
|
|
350
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND
|
|
351
|
+
}, 400);
|
|
352
|
+
const subscriptionToUpdate = subscriptionId ? await ctx.get("context").adapter.findOne({
|
|
353
|
+
model: "subscription",
|
|
354
|
+
where: [{
|
|
355
|
+
field: "id",
|
|
356
|
+
value: subscriptionId,
|
|
357
|
+
connector: "OR"
|
|
358
|
+
}, {
|
|
359
|
+
field: "stripeSubscriptionId",
|
|
360
|
+
value: subscriptionId,
|
|
361
|
+
connector: "OR"
|
|
362
|
+
}]
|
|
363
|
+
}) : referenceId ? await ctx.get("context").adapter.findOne({
|
|
364
|
+
model: "subscription",
|
|
365
|
+
where: [{
|
|
366
|
+
field: "referenceId",
|
|
367
|
+
value: referenceId
|
|
368
|
+
}]
|
|
369
|
+
}) : null;
|
|
370
|
+
if (subscriptionId && !subscriptionToUpdate) return ctx.json({
|
|
371
|
+
success: False,
|
|
372
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
|
|
373
|
+
}, 400);
|
|
374
|
+
let customerId = subscriptionToUpdate?.stripeCustomerId || user$1.stripeCustomerId;
|
|
375
|
+
if (!customerId) try {
|
|
376
|
+
let stripeCustomer = (await client.customers.list({
|
|
377
|
+
email: user$1.email,
|
|
378
|
+
limit: 1
|
|
379
|
+
})).data[0];
|
|
380
|
+
if (!stripeCustomer) stripeCustomer = await client.customers.create({
|
|
381
|
+
email: user$1.email,
|
|
382
|
+
...user$1.name && { name: user$1.name },
|
|
383
|
+
metadata: {
|
|
384
|
+
...metadata,
|
|
385
|
+
userId: user$1.id
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
await ctx.get("context").adapter.update({
|
|
389
|
+
model: "user",
|
|
390
|
+
update: { stripeCustomerId: stripeCustomer.id },
|
|
391
|
+
where: [{
|
|
392
|
+
field: "id",
|
|
393
|
+
value: user$1.id
|
|
394
|
+
}]
|
|
395
|
+
});
|
|
396
|
+
customerId = stripeCustomer.id;
|
|
397
|
+
} catch (e) {
|
|
398
|
+
ctx.get("context").logger.error(e);
|
|
399
|
+
return ctx.json({
|
|
400
|
+
success: False,
|
|
401
|
+
message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER
|
|
402
|
+
}, 400);
|
|
403
|
+
}
|
|
404
|
+
const activeSubscription = (await client.subscriptions.list({ customer: customerId }).then((res$1) => res$1.data.filter((sub) => sub.status === "active" || sub.status === "trialing"))).find((sub) => subscriptionToUpdate?.stripeSubscriptionId || subscriptionId ? sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === subscriptionId : false);
|
|
405
|
+
const subscriptions$1 = subscriptionToUpdate ? [subscriptionToUpdate] : await ctx.get("context").adapter.findMany({
|
|
406
|
+
model: "subscription",
|
|
407
|
+
where: [{
|
|
408
|
+
field: "referenceId",
|
|
409
|
+
value: referenceId || user$1.id
|
|
410
|
+
}]
|
|
411
|
+
});
|
|
412
|
+
const activeOrTrialingSubscription = subscriptions$1.find((sub) => sub.status === "active" || sub.status === "trialing");
|
|
413
|
+
const incompleteSubscription = subscriptions$1.find((sub) => sub.status === "incomplete");
|
|
414
|
+
if (activeOrTrialingSubscription && activeOrTrialingSubscription.status === "active" && activeOrTrialingSubscription.plan === requestPlan && activeOrTrialingSubscription.seats === (seats || 1)) return ctx.json({
|
|
415
|
+
success: False,
|
|
416
|
+
message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
|
|
417
|
+
}, 400);
|
|
418
|
+
if (activeSubscription && customerId) {
|
|
419
|
+
const res$1 = await client.billingPortal.sessions.create({
|
|
420
|
+
customer: customerId,
|
|
421
|
+
return_url: getUrl(authOptions, returnUrl || "/"),
|
|
422
|
+
flow_data: {
|
|
423
|
+
type: "subscription_update_confirm",
|
|
424
|
+
after_completion: {
|
|
425
|
+
type: "redirect",
|
|
426
|
+
redirect: { return_url: getUrl(authOptions, returnUrl || "/") }
|
|
427
|
+
},
|
|
428
|
+
subscription_update_confirm: {
|
|
429
|
+
subscription: activeSubscription.id,
|
|
430
|
+
items: [{
|
|
431
|
+
id: activeSubscription.items.data[0]?.id,
|
|
432
|
+
quantity: seats || 1,
|
|
433
|
+
price: annual ? plan.annualDiscountPriceId : plan.priceId
|
|
434
|
+
}]
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}).catch(async (e) => ctx.json({
|
|
438
|
+
success: False,
|
|
439
|
+
message: e.message,
|
|
440
|
+
code: e.code
|
|
441
|
+
}, 400));
|
|
442
|
+
if (res$1 instanceof Response) return res$1;
|
|
443
|
+
return ctx.json({
|
|
444
|
+
url: res$1.url,
|
|
445
|
+
redirect: True
|
|
446
|
+
}, 200);
|
|
447
|
+
}
|
|
448
|
+
let subscription = activeOrTrialingSubscription || incompleteSubscription;
|
|
449
|
+
if (incompleteSubscription && !activeOrTrialingSubscription) subscription = await ctx.get("context").adapter.update({
|
|
450
|
+
model: "subscription",
|
|
451
|
+
update: {
|
|
452
|
+
plan: plan.name.toLowerCase(),
|
|
453
|
+
seats: seats || 1,
|
|
454
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
455
|
+
},
|
|
456
|
+
where: [{
|
|
457
|
+
field: "id",
|
|
458
|
+
value: incompleteSubscription.id
|
|
459
|
+
}]
|
|
460
|
+
}) || incompleteSubscription;
|
|
461
|
+
if (!subscription) subscription = await ctx.get("context").adapter.create({
|
|
462
|
+
model: "subscription",
|
|
463
|
+
data: {
|
|
464
|
+
plan: plan.name.toLowerCase(),
|
|
465
|
+
stripeCustomerId: customerId,
|
|
466
|
+
status: "incomplete",
|
|
467
|
+
referenceId,
|
|
468
|
+
seats: seats || 1
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
if (!subscription) {
|
|
472
|
+
ctx.get("context").logger.error("Subscription ID not found");
|
|
473
|
+
return ctx.json({ success: False }, 500);
|
|
474
|
+
}
|
|
475
|
+
const params = await options.subscription?.getCheckoutSessionParams?.({
|
|
476
|
+
user: user$1,
|
|
477
|
+
session,
|
|
478
|
+
plan,
|
|
479
|
+
subscription
|
|
480
|
+
}, ctx);
|
|
481
|
+
const freeTrial = !subscriptions$1.some((s) => {
|
|
482
|
+
return !!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
483
|
+
}) && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
|
|
484
|
+
let priceIdToUse = void 0;
|
|
485
|
+
if (annual) {
|
|
486
|
+
priceIdToUse = plan.annualDiscountPriceId;
|
|
487
|
+
if (!priceIdToUse && plan.annualDiscountLookupKey) priceIdToUse = await resolvePriceIdFromLookupKey(client, plan.annualDiscountLookupKey);
|
|
488
|
+
} else {
|
|
489
|
+
priceIdToUse = plan.priceId;
|
|
490
|
+
if (!priceIdToUse && plan.lookupKey) priceIdToUse = await resolvePriceIdFromLookupKey(client, plan.lookupKey);
|
|
491
|
+
}
|
|
492
|
+
const checkoutSession = await client.checkout.sessions.create({
|
|
493
|
+
...customerId ? {
|
|
494
|
+
customer: customerId,
|
|
495
|
+
customer_update: {
|
|
496
|
+
name: "auto",
|
|
497
|
+
address: "auto"
|
|
498
|
+
}
|
|
499
|
+
} : { customer_email: user$1.email },
|
|
500
|
+
success_url: getUrl(authOptions, `${ctx.get("context").baseURL}/subscription/success?callbackURL=${encodeURIComponent(successUrl)}&subscriptionId=${encodeURIComponent(subscription.id)}`),
|
|
501
|
+
cancel_url: getUrl(authOptions, cancelUrl),
|
|
502
|
+
line_items: [{
|
|
503
|
+
price: priceIdToUse,
|
|
504
|
+
quantity: seats || 1
|
|
505
|
+
}],
|
|
506
|
+
subscription_data: { ...freeTrial },
|
|
507
|
+
mode: "subscription",
|
|
508
|
+
client_reference_id: referenceId,
|
|
509
|
+
...params?.params,
|
|
510
|
+
metadata: {
|
|
511
|
+
userId: user$1.id,
|
|
512
|
+
subscriptionId: subscription.id,
|
|
513
|
+
referenceId,
|
|
514
|
+
...params?.params?.metadata
|
|
515
|
+
}
|
|
516
|
+
}, params?.options).catch(async (e) => ctx.json({
|
|
517
|
+
success: False,
|
|
518
|
+
message: e.message,
|
|
519
|
+
code: e.code
|
|
520
|
+
}, 400));
|
|
521
|
+
if (checkoutSession instanceof Response) return checkoutSession;
|
|
522
|
+
return ctx.json({
|
|
523
|
+
...checkoutSession,
|
|
524
|
+
redirect: !disableRedirect
|
|
525
|
+
}, 200);
|
|
526
|
+
}),
|
|
527
|
+
cancelSubscriptionCallback: createEndpoint(createRoute({
|
|
528
|
+
operationId: "cancelSubscriptionCallback",
|
|
529
|
+
method: "get",
|
|
530
|
+
path: "/subscription/cancel/callback",
|
|
531
|
+
middleware: [originCheck((ctx) => ctx.req.query("callbackURL"))],
|
|
532
|
+
request: req().qry(z.object({
|
|
533
|
+
callbackURL: callbackURLSchema(true),
|
|
534
|
+
subscriptionId: z.string().optional()
|
|
535
|
+
})).bld(),
|
|
536
|
+
responses: res().rdr().bld()
|
|
537
|
+
}), (authOptions) => async (ctx) => {
|
|
538
|
+
if (!options.subscription?.enabled) {
|
|
539
|
+
logger.error(`Subscription actions are not enabled in your stripe plugin config.`);
|
|
540
|
+
return ctx.json({ success: False }, 400);
|
|
541
|
+
}
|
|
542
|
+
const { callbackURL, subscriptionId } = ctx.req.valid("query");
|
|
543
|
+
if (!callbackURL || !subscriptionId) return ctx.redirect(getUrl(authOptions, callbackURL || "/"), 302);
|
|
544
|
+
const session = await getSessionFromCtx(ctx);
|
|
545
|
+
if (session instanceof Response) return ctx.redirect(getUrl(authOptions, callbackURL || "/"), 302);
|
|
546
|
+
if (session.user?.stripeCustomerId) try {
|
|
547
|
+
const subscription = await ctx.get("context").adapter.findOne({
|
|
548
|
+
model: "subscription",
|
|
549
|
+
where: [{
|
|
550
|
+
field: "id",
|
|
551
|
+
value: subscriptionId
|
|
552
|
+
}]
|
|
553
|
+
});
|
|
554
|
+
if (!subscription || subscription.cancelAtPeriodEnd || subscription.status === "canceled") return ctx.redirect(getUrl(authOptions, callbackURL), 302);
|
|
555
|
+
const currentSubscription = (await client.subscriptions.list({
|
|
556
|
+
customer: session.user.stripeCustomerId,
|
|
557
|
+
status: "active"
|
|
558
|
+
})).data.find((sub) => sub.id === subscription.stripeSubscriptionId);
|
|
559
|
+
if (currentSubscription?.cancel_at_period_end === true) {
|
|
560
|
+
await ctx.get("context").adapter.update({
|
|
561
|
+
model: "subscription",
|
|
562
|
+
update: {
|
|
563
|
+
status: currentSubscription?.status,
|
|
564
|
+
cancelAtPeriodEnd: true
|
|
565
|
+
},
|
|
566
|
+
where: [{
|
|
567
|
+
field: "id",
|
|
568
|
+
value: subscription.id
|
|
569
|
+
}]
|
|
570
|
+
});
|
|
571
|
+
await options.subscription?.onSubscriptionCancel?.({
|
|
572
|
+
subscription,
|
|
573
|
+
cancellationDetails: currentSubscription.cancellation_details,
|
|
574
|
+
stripeSubscription: currentSubscription,
|
|
575
|
+
event: void 0
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
ctx.get("context").logger.error("Error checking subscription status from Stripe", error);
|
|
580
|
+
}
|
|
581
|
+
return ctx.redirect(getUrl(authOptions, callbackURL), 302);
|
|
582
|
+
}),
|
|
583
|
+
cancelSubscription: createEndpoint(createRoute({
|
|
584
|
+
operationId: "cancelSubscription",
|
|
585
|
+
method: "post",
|
|
586
|
+
path: "/subscription/cancel",
|
|
587
|
+
middleware: [
|
|
588
|
+
sessionMiddleware(),
|
|
589
|
+
originCheck((ctx) => ctx.req.raw.clone().json().then((v) => v.returnUrl)),
|
|
590
|
+
referenceMiddleware("cancel-subscription")
|
|
591
|
+
],
|
|
592
|
+
request: req().bdy(z.object({
|
|
593
|
+
referenceId: z.string().meta({ description: "Reference id of the subscription to cancel. Eg: '123'" }).optional(),
|
|
594
|
+
subscriptionId: z.string().meta({ description: "The id of the subscription to cancel. Eg: 'sub_123'" }).optional(),
|
|
595
|
+
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: \"https://example.com/dashboard\"" })
|
|
596
|
+
})).bld(),
|
|
597
|
+
responses: res(z.object({
|
|
598
|
+
url: z.string(),
|
|
599
|
+
redirect: z.literal(true)
|
|
600
|
+
})).err(400).err(401).bld()
|
|
601
|
+
}), (authOptions) => async (ctx) => {
|
|
602
|
+
const session = ctx.get("session");
|
|
603
|
+
const { referenceId = session.user.id, subscriptionId, returnUrl } = ctx.req.valid("json");
|
|
604
|
+
const subscription = subscriptionId ? await ctx.get("context").adapter.findOne({
|
|
605
|
+
model: "subscription",
|
|
606
|
+
where: [{
|
|
607
|
+
field: "id",
|
|
608
|
+
value: subscriptionId
|
|
609
|
+
}]
|
|
610
|
+
}) : await ctx.get("context").adapter.findMany({
|
|
611
|
+
model: "subscription",
|
|
612
|
+
where: [{
|
|
613
|
+
field: "referenceId",
|
|
614
|
+
value: referenceId
|
|
615
|
+
}]
|
|
616
|
+
}).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
|
|
617
|
+
if (!subscription || !subscription.stripeCustomerId) return ctx.json({
|
|
618
|
+
success: False,
|
|
619
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
|
|
620
|
+
}, 400);
|
|
621
|
+
const activeSubscriptions = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res$2) => res$2.data.filter((sub) => sub.status === "active" || sub.status === "trialing"));
|
|
622
|
+
if (!activeSubscriptions.length) {
|
|
623
|
+
/**
|
|
624
|
+
* If the subscription is not found, we need to delete the subscription
|
|
625
|
+
* from the database. This is a rare case and should not happen.
|
|
626
|
+
*/
|
|
627
|
+
await ctx.get("context").adapter.deleteMany({
|
|
628
|
+
model: "subscription",
|
|
629
|
+
where: [{
|
|
630
|
+
field: "referenceId",
|
|
631
|
+
value: referenceId
|
|
632
|
+
}]
|
|
633
|
+
});
|
|
634
|
+
return ctx.json({
|
|
635
|
+
success: False,
|
|
636
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
|
|
637
|
+
}, 400);
|
|
638
|
+
}
|
|
639
|
+
const activeSubscription = activeSubscriptions.find((sub) => sub.id === subscription.stripeSubscriptionId);
|
|
640
|
+
if (!activeSubscription) return ctx.json({
|
|
641
|
+
success: False,
|
|
642
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
|
|
643
|
+
}, 400);
|
|
644
|
+
const res$1 = await client.billingPortal.sessions.create({
|
|
645
|
+
customer: subscription.stripeCustomerId,
|
|
646
|
+
return_url: getUrl(authOptions, `${ctx.get("context").baseURL}/subscription/cancel/callback?callbackURL=${encodeURIComponent(returnUrl || "/")}&subscriptionId=${encodeURIComponent(subscription.id)}`),
|
|
647
|
+
flow_data: {
|
|
648
|
+
type: "subscription_cancel",
|
|
649
|
+
subscription_cancel: { subscription: activeSubscription.id }
|
|
650
|
+
}
|
|
651
|
+
}).catch(async (e) => {
|
|
652
|
+
if (e.message.includes("already set to be cancel")) {
|
|
653
|
+
/**
|
|
654
|
+
* incase we missed the event from stripe, we set it manually
|
|
655
|
+
* this is a rare case and should not happen
|
|
656
|
+
*/
|
|
657
|
+
if (!subscription.cancelAtPeriodEnd) await ctx.get("context").adapter.update({
|
|
658
|
+
model: "subscription",
|
|
659
|
+
update: { cancelAtPeriodEnd: true },
|
|
660
|
+
where: [{
|
|
661
|
+
field: "referenceId",
|
|
662
|
+
value: referenceId
|
|
663
|
+
}]
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return ctx.json({
|
|
667
|
+
success: False,
|
|
668
|
+
message: e.message,
|
|
669
|
+
code: e.code
|
|
670
|
+
}, 400);
|
|
671
|
+
});
|
|
672
|
+
if (res$1 instanceof Response) return res$1;
|
|
673
|
+
return ctx.json({
|
|
674
|
+
url: res$1.url,
|
|
675
|
+
redirect: True
|
|
676
|
+
}, 200);
|
|
677
|
+
}),
|
|
678
|
+
restoreSubscription: createEndpoint(createRoute({
|
|
679
|
+
operationId: "restoreSubscription",
|
|
680
|
+
method: "post",
|
|
681
|
+
path: "/subscription/restore",
|
|
682
|
+
middleware: [sessionMiddleware(), referenceMiddleware("restore-subscription")],
|
|
683
|
+
request: req().bdy(z.object({
|
|
684
|
+
referenceId: z.string().meta({ description: "Reference id of the subscription to restore. Eg: '123'" }).optional(),
|
|
685
|
+
subscriptionId: z.string().meta({ description: "The id of the subscription to restore. Eg: 'sub_123'" }).optional()
|
|
686
|
+
})).bld(),
|
|
687
|
+
responses: res(z.looseObject({})).err(400).err(401).bld()
|
|
688
|
+
}), (authOptions) => async (ctx) => {
|
|
689
|
+
const session = ctx.get("session");
|
|
690
|
+
const { referenceId = session.user.id, subscriptionId } = ctx.req.valid("json");
|
|
691
|
+
const subscription = subscriptionId ? await ctx.get("context").adapter.findOne({
|
|
692
|
+
model: "subscription",
|
|
693
|
+
where: [{
|
|
694
|
+
field: "id",
|
|
695
|
+
value: subscriptionId
|
|
696
|
+
}]
|
|
697
|
+
}) : await ctx.get("context").adapter.findMany({
|
|
698
|
+
model: "subscription",
|
|
699
|
+
where: [{
|
|
700
|
+
field: "referenceId",
|
|
701
|
+
value: referenceId
|
|
702
|
+
}]
|
|
703
|
+
}).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing"));
|
|
704
|
+
if (!subscription || !subscription.stripeCustomerId) return ctx.json({
|
|
705
|
+
success: False,
|
|
706
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
|
|
707
|
+
}, 400);
|
|
708
|
+
if (subscription.status != "active" && subscription.status != "trialing") return ctx.json({
|
|
709
|
+
success: False,
|
|
710
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_ACTIVE
|
|
711
|
+
}, 400);
|
|
712
|
+
if (!subscription.cancelAtPeriodEnd) return ctx.json({
|
|
713
|
+
success: False,
|
|
714
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION
|
|
715
|
+
}, 400);
|
|
716
|
+
const activeSubscription = await client.subscriptions.list({ customer: subscription.stripeCustomerId }).then((res$1) => res$1.data.filter((sub) => sub.status === "active" || sub.status === "trialing")[0]);
|
|
717
|
+
if (!activeSubscription) return ctx.json({
|
|
718
|
+
success: False,
|
|
719
|
+
message: STRIPE_ERROR_CODES.SUBSCRIPTION_NOT_FOUND
|
|
720
|
+
}, 400);
|
|
721
|
+
try {
|
|
722
|
+
const newSub = await client.subscriptions.update(activeSubscription.id, { cancel_at_period_end: false });
|
|
723
|
+
await ctx.get("context").adapter.update({
|
|
724
|
+
model: "subscription",
|
|
725
|
+
update: {
|
|
726
|
+
cancelAtPeriodEnd: false,
|
|
727
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
728
|
+
},
|
|
729
|
+
where: [{
|
|
730
|
+
field: "id",
|
|
731
|
+
value: subscription.id
|
|
732
|
+
}]
|
|
733
|
+
});
|
|
734
|
+
return ctx.json(newSub, 200);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
ctx.get("context").logger.error("Error restoring subscription", error);
|
|
737
|
+
return ctx.json({
|
|
738
|
+
success: False,
|
|
739
|
+
message: STRIPE_ERROR_CODES.UNABLE_TO_CREATE_CUSTOMER
|
|
740
|
+
}, 400);
|
|
741
|
+
}
|
|
742
|
+
}),
|
|
743
|
+
listActiveSubscriptions: createEndpoint(createRoute({
|
|
744
|
+
operationId: "listActiveSubscriptions",
|
|
745
|
+
method: "get",
|
|
746
|
+
path: "/subscription/list",
|
|
747
|
+
middleware: [sessionMiddleware(), referenceMiddleware("list-subscription")],
|
|
748
|
+
request: req().qry(z.object({ referenceId: z.string().meta({ description: "Reference id of the subscription to list. Eg: '123'" }).optional() })).bld(),
|
|
749
|
+
responses: res(z.array(z.looseObject({}))).err(400).err(401).bld()
|
|
750
|
+
}), (_authOptions) => async (ctx) => {
|
|
751
|
+
if (!options.subscription?.enabled) {
|
|
752
|
+
logger.error(`Subscription actions are not enabled in your stripe plugin config.`);
|
|
753
|
+
return ctx.json({ success: False }, 400);
|
|
754
|
+
}
|
|
755
|
+
const session = ctx.get("session");
|
|
756
|
+
const { referenceId = session.user.id } = ctx.req.valid("query");
|
|
757
|
+
const subscriptions$1 = await ctx.get("context").adapter.findMany({
|
|
758
|
+
model: "subscription",
|
|
759
|
+
where: [{
|
|
760
|
+
field: "referenceId",
|
|
761
|
+
value: referenceId
|
|
762
|
+
}]
|
|
763
|
+
});
|
|
764
|
+
if (!subscriptions$1.length) return ctx.json([], 200);
|
|
765
|
+
const plans = await getPlans(options.subscription);
|
|
766
|
+
if (!plans) return ctx.json([], 200);
|
|
767
|
+
const subs = subscriptions$1.map((sub) => {
|
|
768
|
+
const plan = plans.find((p) => p.name.toLowerCase() === sub.plan.toLowerCase());
|
|
769
|
+
return {
|
|
770
|
+
...sub,
|
|
771
|
+
limits: plan?.limits,
|
|
772
|
+
priceId: plan?.priceId
|
|
773
|
+
};
|
|
774
|
+
}).filter((sub) => sub.status === "active" || sub.status === "trialing");
|
|
775
|
+
return ctx.json(subs, 200);
|
|
776
|
+
}),
|
|
777
|
+
subscriptionSuccess: createEndpoint(createRoute({
|
|
778
|
+
operationId: "subscriptionSuccess",
|
|
779
|
+
method: "get",
|
|
780
|
+
path: "/subscription/success",
|
|
781
|
+
middleware: [originCheck((ctx) => ctx.req.query("callbackURL"))],
|
|
782
|
+
request: req().qry(z.object({
|
|
783
|
+
callbackURL: callbackURLSchema(true),
|
|
784
|
+
subscriptionId: z.string().optional()
|
|
785
|
+
})).bld(),
|
|
786
|
+
responses: res().rdr().bld()
|
|
787
|
+
}), (authOptions) => async (ctx) => {
|
|
788
|
+
const { callbackURL, subscriptionId } = ctx.req.valid("query");
|
|
789
|
+
if (!callbackURL || !subscriptionId) return ctx.redirect(getUrl(authOptions, callbackURL || "/"), 302);
|
|
790
|
+
const session = await getSessionFromCtx(ctx);
|
|
791
|
+
if (session instanceof Response) return ctx.redirect(getUrl(authOptions, callbackURL || "/"), 302);
|
|
792
|
+
const subscription = await ctx.get("context").adapter.findOne({
|
|
793
|
+
model: "subscription",
|
|
794
|
+
where: [{
|
|
795
|
+
field: "id",
|
|
796
|
+
value: subscriptionId
|
|
797
|
+
}]
|
|
798
|
+
});
|
|
799
|
+
if (subscription?.status === "active" || subscription?.status === "trialing") return ctx.redirect(getUrl(authOptions, callbackURL), 302);
|
|
800
|
+
const customerId = subscription?.stripeCustomerId || session.user.stripeCustomerId;
|
|
801
|
+
if (customerId) try {
|
|
802
|
+
const stripeSubscription = await client.subscriptions.list({
|
|
803
|
+
customer: customerId,
|
|
804
|
+
status: "active"
|
|
805
|
+
}).then((res$1) => res$1.data[0]);
|
|
806
|
+
if (stripeSubscription) {
|
|
807
|
+
const firstItem = stripeSubscription.items.data[0];
|
|
808
|
+
if (firstItem) {
|
|
809
|
+
const plan = await getPlanByPriceInfo(options, firstItem.price.id, firstItem.price.lookup_key);
|
|
810
|
+
if (plan && subscription) await ctx.get("context").adapter.update({
|
|
811
|
+
model: "subscription",
|
|
812
|
+
update: {
|
|
813
|
+
status: stripeSubscription.status,
|
|
814
|
+
seats: firstItem.quantity || 1,
|
|
815
|
+
plan: plan.name.toLowerCase(),
|
|
816
|
+
periodEnd: /* @__PURE__ */ new Date(firstItem.current_period_end * 1e3),
|
|
817
|
+
periodStart: /* @__PURE__ */ new Date(firstItem.current_period_start * 1e3),
|
|
818
|
+
stripeSubscriptionId: stripeSubscription.id,
|
|
819
|
+
...stripeSubscription.trial_start && stripeSubscription.trial_end ? {
|
|
820
|
+
trialStart: /* @__PURE__ */ new Date(stripeSubscription.trial_start * 1e3),
|
|
821
|
+
trialEnd: /* @__PURE__ */ new Date(stripeSubscription.trial_end * 1e3)
|
|
822
|
+
} : {}
|
|
823
|
+
},
|
|
824
|
+
where: [{
|
|
825
|
+
field: "id",
|
|
826
|
+
value: subscription.id
|
|
827
|
+
}]
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
} catch (error) {
|
|
832
|
+
ctx.get("context").logger.error("Error fetching subscription from Stripe", error);
|
|
833
|
+
}
|
|
834
|
+
return ctx.redirect(getUrl(authOptions, callbackURL), 302);
|
|
835
|
+
}),
|
|
836
|
+
createBillingPortal: createEndpoint(createRoute({
|
|
837
|
+
operationId: "createBillingPortal",
|
|
838
|
+
method: "post",
|
|
839
|
+
path: "/subscription/billing-portal",
|
|
840
|
+
middleware: [
|
|
841
|
+
sessionMiddleware(),
|
|
842
|
+
originCheck((ctx) => ctx.req.raw.clone().json().then((v) => v.returnUrl)),
|
|
843
|
+
referenceMiddleware("billing-portal")
|
|
844
|
+
],
|
|
845
|
+
request: req().bdy(z.object({
|
|
846
|
+
locale: z.custom((localization) => {
|
|
847
|
+
return typeof localization === "string";
|
|
848
|
+
}).optional(),
|
|
849
|
+
referenceId: z.string().optional(),
|
|
850
|
+
returnUrl: z.string().default("/")
|
|
851
|
+
})).bld(),
|
|
852
|
+
responses: res(z.object({
|
|
853
|
+
url: z.string(),
|
|
854
|
+
redirect: z.literal(true)
|
|
855
|
+
})).err(400).err(401).bld()
|
|
856
|
+
}), (authOptions) => async (ctx) => {
|
|
857
|
+
const session = ctx.get("session");
|
|
858
|
+
const { referenceId = session.user.id, locale, returnUrl } = ctx.req.valid("json");
|
|
859
|
+
let customerId = session.user.stripeCustomerId;
|
|
860
|
+
if (!customerId) customerId = (await ctx.get("context").adapter.findMany({
|
|
861
|
+
model: "subscription",
|
|
862
|
+
where: [{
|
|
863
|
+
field: "referenceId",
|
|
864
|
+
value: referenceId
|
|
865
|
+
}]
|
|
866
|
+
}).then((subs) => subs.find((sub) => sub.status === "active" || sub.status === "trialing")))?.stripeCustomerId;
|
|
867
|
+
if (!customerId) return ctx.json({
|
|
868
|
+
success: False,
|
|
869
|
+
message: "No Stripe customer found for this user"
|
|
870
|
+
}, 400);
|
|
871
|
+
try {
|
|
872
|
+
const { url } = await client.billingPortal.sessions.create({
|
|
873
|
+
customer: customerId,
|
|
874
|
+
return_url: getUrl(authOptions, returnUrl),
|
|
875
|
+
...locale && { locale }
|
|
876
|
+
});
|
|
877
|
+
return ctx.json({
|
|
878
|
+
url,
|
|
879
|
+
redirect: True
|
|
880
|
+
}, 200);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
ctx.get("context").logger.error("Error creating billing portal session", error);
|
|
883
|
+
return ctx.json({
|
|
884
|
+
success: False,
|
|
885
|
+
message: error.message
|
|
886
|
+
}, 400);
|
|
887
|
+
}
|
|
888
|
+
})
|
|
889
|
+
};
|
|
890
|
+
return {
|
|
891
|
+
id: "stripe",
|
|
892
|
+
routes: {
|
|
893
|
+
stripeWebhook: createEndpoint(createRoute({
|
|
894
|
+
operationId: "stripeWebhook",
|
|
895
|
+
method: "post",
|
|
896
|
+
path: "/stripe/webhook",
|
|
897
|
+
client: false,
|
|
898
|
+
responses: res(z.object({ success: z.literal(true) })).err(400).err(500).bld()
|
|
899
|
+
}), (_authOptions) => async (ctx) => {
|
|
900
|
+
if (!ctx.req.raw?.body) return ctx.json({ success: False }, 500);
|
|
901
|
+
const buf = await ctx.req.text();
|
|
902
|
+
const sig = ctx.req.header("stripe-signature");
|
|
903
|
+
const webhookSecret = options.stripeWebhookSecret;
|
|
904
|
+
let event;
|
|
905
|
+
try {
|
|
906
|
+
if (!sig || !webhookSecret) return ctx.json({
|
|
907
|
+
success: False,
|
|
908
|
+
message: "Stripe webhook secret not found"
|
|
909
|
+
}, 400);
|
|
910
|
+
event = await client.webhooks.constructEventAsync(buf, sig, webhookSecret);
|
|
911
|
+
} catch (err) {
|
|
912
|
+
ctx.get("context").logger.error(`${err.message}`);
|
|
913
|
+
return ctx.json({
|
|
914
|
+
success: False,
|
|
915
|
+
message: `Webhook Error: ${err.message}`
|
|
916
|
+
}, 400);
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
switch (event.type) {
|
|
920
|
+
case "checkout.session.completed":
|
|
921
|
+
await onCheckoutSessionCompleted(ctx, options, event);
|
|
922
|
+
await options.onEvent?.(event);
|
|
923
|
+
break;
|
|
924
|
+
case "customer.subscription.updated":
|
|
925
|
+
await onSubscriptionUpdated(ctx, options, event);
|
|
926
|
+
await options.onEvent?.(event);
|
|
927
|
+
break;
|
|
928
|
+
case "customer.subscription.deleted":
|
|
929
|
+
await onSubscriptionDeleted(ctx, options, event);
|
|
930
|
+
await options.onEvent?.(event);
|
|
931
|
+
break;
|
|
932
|
+
default:
|
|
933
|
+
await options.onEvent?.(event);
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
} catch (e) {
|
|
937
|
+
ctx.get("context").logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
938
|
+
return ctx.json({
|
|
939
|
+
success: False,
|
|
940
|
+
message: "Webhook error: See server logs for more information."
|
|
941
|
+
}, 400);
|
|
942
|
+
}
|
|
943
|
+
return ctx.json({ success: True }, 200);
|
|
944
|
+
}),
|
|
945
|
+
...options.subscription?.enabled ? subscriptionEndpoints : {}
|
|
946
|
+
},
|
|
947
|
+
init(ctx) {
|
|
948
|
+
return { options: { databaseHooks: { user: { create: { async after(user$1, ctx$1) {
|
|
949
|
+
if (ctx$1 && options.createCustomerOnSignUp) {
|
|
950
|
+
const stripeCustomer = await client.customers.create({
|
|
951
|
+
email: user$1.email,
|
|
952
|
+
...user$1.name && { name: user$1.name },
|
|
953
|
+
metadata: { userId: user$1.id }
|
|
954
|
+
});
|
|
955
|
+
if (!await ctx$1.get("context").internalAdapter.updateUser(user$1.id, { stripeCustomerId: stripeCustomer.id })) logger.error("#FAIRE_AUTH: Failed to create customer");
|
|
956
|
+
else if (options.onCustomerCreate) await options.onCustomerCreate({
|
|
957
|
+
stripeCustomer,
|
|
958
|
+
user: user$1
|
|
959
|
+
}, ctx$1);
|
|
960
|
+
}
|
|
961
|
+
} } } } } };
|
|
962
|
+
},
|
|
963
|
+
schema: getSchema(options)
|
|
964
|
+
};
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
//#endregion
|
|
968
|
+
export { stripe };
|