@better-auth/stripe 1.3.9 → 1.3.10-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 +4 -4
- package/dist/index.cjs +49 -20
- package/dist/index.mjs +49 -20
- package/package.json +4 -3
- package/src/hooks.ts +10 -3
- package/src/index.ts +45 -14
- package/src/stripe.test.ts +178 -0
- package/src/utils.ts +7 -2
- package/vitest.config.ts +0 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/stripe@1.3.
|
|
2
|
+
> @better-auth/stripe@1.3.10-beta.2 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: 45.4 kB, chunk size: 45.4 kB, exports: stripe)
|
|
9
9
|
|
|
10
10
|
[log] dist/client.cjs (total size: 224 B, chunk size: 224 B, exports: stripeClient)
|
|
11
11
|
|
|
12
|
-
[log] dist/index.mjs (total size:
|
|
12
|
+
[log] dist/index.mjs (total size: 44.5 kB, chunk size: 44.5 kB, exports: stripe)
|
|
13
13
|
|
|
14
14
|
[log] dist/client.mjs (total size: 197 B, chunk size: 197 B, exports: stripeClient)
|
|
15
15
|
|
|
16
|
-
Σ Total dist size (byte size):
|
|
16
|
+
Σ Total dist size (byte size): 227 kB
|
|
17
17
|
[log]
|
package/dist/index.cjs
CHANGED
|
@@ -23,10 +23,10 @@ const z__namespace = /*#__PURE__*/_interopNamespaceCompat(z);
|
|
|
23
23
|
async function getPlans(options) {
|
|
24
24
|
return typeof options?.subscription?.plans === "function" ? await options.subscription?.plans() : options.subscription?.plans;
|
|
25
25
|
}
|
|
26
|
-
async function
|
|
26
|
+
async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
|
|
27
27
|
return await getPlans(options).then(
|
|
28
28
|
(res) => res?.find(
|
|
29
|
-
(plan) => plan.priceId === priceId || plan.annualDiscountPriceId === priceId
|
|
29
|
+
(plan) => plan.priceId === priceId || plan.annualDiscountPriceId === priceId || priceLookupKey && (plan.lookupKey === priceLookupKey || plan.annualDiscountLookupKey === priceLookupKey)
|
|
30
30
|
)
|
|
31
31
|
);
|
|
32
32
|
}
|
|
@@ -47,7 +47,12 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
47
47
|
checkoutSession.subscription
|
|
48
48
|
);
|
|
49
49
|
const priceId = subscription.items.data[0]?.price.id;
|
|
50
|
-
const
|
|
50
|
+
const priceLookupKey = subscription.items.data[0]?.price.lookup_key || null;
|
|
51
|
+
const plan = await getPlanByPriceInfo(
|
|
52
|
+
options,
|
|
53
|
+
priceId,
|
|
54
|
+
priceLookupKey
|
|
55
|
+
);
|
|
51
56
|
if (plan) {
|
|
52
57
|
const referenceId = checkoutSession?.client_reference_id || checkoutSession?.metadata?.referenceId;
|
|
53
58
|
const subscriptionId = checkoutSession?.metadata?.subscriptionId;
|
|
@@ -117,7 +122,8 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
117
122
|
}
|
|
118
123
|
const subscriptionUpdated = event.data.object;
|
|
119
124
|
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
120
|
-
const
|
|
125
|
+
const priceLookupKey = subscriptionUpdated.items.data[0].price.lookup_key || null;
|
|
126
|
+
const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
|
|
121
127
|
const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
|
|
122
128
|
const customerId = subscriptionUpdated.customer?.toString();
|
|
123
129
|
let subscription = await ctx.context.adapter.findOne({
|
|
@@ -563,10 +569,13 @@ const stripe = (options) => {
|
|
|
563
569
|
}
|
|
564
570
|
]
|
|
565
571
|
});
|
|
566
|
-
const
|
|
572
|
+
const activeOrTrialingSubscription = subscriptions.find(
|
|
567
573
|
(sub) => sub.status === "active" || sub.status === "trialing"
|
|
568
574
|
);
|
|
569
|
-
|
|
575
|
+
const incompleteSubscription = subscriptions.find(
|
|
576
|
+
(sub) => sub.status === "incomplete"
|
|
577
|
+
);
|
|
578
|
+
if (activeOrTrialingSubscription && activeOrTrialingSubscription.status === "active" && activeOrTrialingSubscription.plan === ctx.body.plan && activeOrTrialingSubscription.seats === (ctx.body.seats || 1)) {
|
|
570
579
|
throw new api.APIError("BAD_REQUEST", {
|
|
571
580
|
message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
|
|
572
581
|
});
|
|
@@ -605,16 +614,36 @@ const stripe = (options) => {
|
|
|
605
614
|
redirect: true
|
|
606
615
|
});
|
|
607
616
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
617
|
+
let subscription = activeOrTrialingSubscription || incompleteSubscription;
|
|
618
|
+
if (incompleteSubscription && !activeOrTrialingSubscription) {
|
|
619
|
+
const updated = await ctx.context.adapter.update({
|
|
620
|
+
model: "subscription",
|
|
621
|
+
update: {
|
|
622
|
+
plan: plan.name.toLowerCase(),
|
|
623
|
+
seats: ctx.body.seats || 1,
|
|
624
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
625
|
+
},
|
|
626
|
+
where: [
|
|
627
|
+
{
|
|
628
|
+
field: "id",
|
|
629
|
+
value: incompleteSubscription.id
|
|
630
|
+
}
|
|
631
|
+
]
|
|
632
|
+
});
|
|
633
|
+
subscription = updated || incompleteSubscription;
|
|
634
|
+
}
|
|
635
|
+
if (!subscription) {
|
|
636
|
+
subscription = await ctx.context.adapter.create({
|
|
637
|
+
model: "subscription",
|
|
638
|
+
data: {
|
|
639
|
+
plan: plan.name.toLowerCase(),
|
|
640
|
+
stripeCustomerId: customerId,
|
|
641
|
+
status: "incomplete",
|
|
642
|
+
referenceId,
|
|
643
|
+
seats: ctx.body.seats || 1
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
618
647
|
if (!subscription) {
|
|
619
648
|
ctx.context.logger.error("Subscription ID not found");
|
|
620
649
|
throw new api.APIError("INTERNAL_SERVER_ERROR");
|
|
@@ -631,9 +660,8 @@ const stripe = (options) => {
|
|
|
631
660
|
ctx
|
|
632
661
|
);
|
|
633
662
|
const hasEverTrialed = subscriptions.some((s) => {
|
|
634
|
-
const samePlan = s.plan?.toLowerCase() === plan.name.toLowerCase();
|
|
635
663
|
const hadTrial = !!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
636
|
-
return
|
|
664
|
+
return hadTrial;
|
|
637
665
|
});
|
|
638
666
|
const freeTrial = !hasEverTrialed && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
|
|
639
667
|
let priceIdToUse = void 0;
|
|
@@ -1097,9 +1125,10 @@ const stripe = (options) => {
|
|
|
1097
1125
|
status: "active"
|
|
1098
1126
|
}).then((res) => res.data[0]);
|
|
1099
1127
|
if (stripeSubscription) {
|
|
1100
|
-
const plan = await
|
|
1128
|
+
const plan = await getPlanByPriceInfo(
|
|
1101
1129
|
options,
|
|
1102
|
-
stripeSubscription.items.data[0]?.
|
|
1130
|
+
stripeSubscription.items.data[0]?.price.id,
|
|
1131
|
+
stripeSubscription.items.data[0]?.price.lookup_key
|
|
1103
1132
|
);
|
|
1104
1133
|
if (plan && subscription) {
|
|
1105
1134
|
await ctx.context.adapter.update({
|
package/dist/index.mjs
CHANGED
|
@@ -7,10 +7,10 @@ import { mergeSchema } from 'better-auth/db';
|
|
|
7
7
|
async function getPlans(options) {
|
|
8
8
|
return typeof options?.subscription?.plans === "function" ? await options.subscription?.plans() : options.subscription?.plans;
|
|
9
9
|
}
|
|
10
|
-
async function
|
|
10
|
+
async function getPlanByPriceInfo(options, priceId, priceLookupKey) {
|
|
11
11
|
return await getPlans(options).then(
|
|
12
12
|
(res) => res?.find(
|
|
13
|
-
(plan) => plan.priceId === priceId || plan.annualDiscountPriceId === priceId
|
|
13
|
+
(plan) => plan.priceId === priceId || plan.annualDiscountPriceId === priceId || priceLookupKey && (plan.lookupKey === priceLookupKey || plan.annualDiscountLookupKey === priceLookupKey)
|
|
14
14
|
)
|
|
15
15
|
);
|
|
16
16
|
}
|
|
@@ -31,7 +31,12 @@ async function onCheckoutSessionCompleted(ctx, options, event) {
|
|
|
31
31
|
checkoutSession.subscription
|
|
32
32
|
);
|
|
33
33
|
const priceId = subscription.items.data[0]?.price.id;
|
|
34
|
-
const
|
|
34
|
+
const priceLookupKey = subscription.items.data[0]?.price.lookup_key || null;
|
|
35
|
+
const plan = await getPlanByPriceInfo(
|
|
36
|
+
options,
|
|
37
|
+
priceId,
|
|
38
|
+
priceLookupKey
|
|
39
|
+
);
|
|
35
40
|
if (plan) {
|
|
36
41
|
const referenceId = checkoutSession?.client_reference_id || checkoutSession?.metadata?.referenceId;
|
|
37
42
|
const subscriptionId = checkoutSession?.metadata?.subscriptionId;
|
|
@@ -101,7 +106,8 @@ async function onSubscriptionUpdated(ctx, options, event) {
|
|
|
101
106
|
}
|
|
102
107
|
const subscriptionUpdated = event.data.object;
|
|
103
108
|
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
104
|
-
const
|
|
109
|
+
const priceLookupKey = subscriptionUpdated.items.data[0].price.lookup_key || null;
|
|
110
|
+
const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
|
|
105
111
|
const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
|
|
106
112
|
const customerId = subscriptionUpdated.customer?.toString();
|
|
107
113
|
let subscription = await ctx.context.adapter.findOne({
|
|
@@ -547,10 +553,13 @@ const stripe = (options) => {
|
|
|
547
553
|
}
|
|
548
554
|
]
|
|
549
555
|
});
|
|
550
|
-
const
|
|
556
|
+
const activeOrTrialingSubscription = subscriptions.find(
|
|
551
557
|
(sub) => sub.status === "active" || sub.status === "trialing"
|
|
552
558
|
);
|
|
553
|
-
|
|
559
|
+
const incompleteSubscription = subscriptions.find(
|
|
560
|
+
(sub) => sub.status === "incomplete"
|
|
561
|
+
);
|
|
562
|
+
if (activeOrTrialingSubscription && activeOrTrialingSubscription.status === "active" && activeOrTrialingSubscription.plan === ctx.body.plan && activeOrTrialingSubscription.seats === (ctx.body.seats || 1)) {
|
|
554
563
|
throw new APIError("BAD_REQUEST", {
|
|
555
564
|
message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN
|
|
556
565
|
});
|
|
@@ -589,16 +598,36 @@ const stripe = (options) => {
|
|
|
589
598
|
redirect: true
|
|
590
599
|
});
|
|
591
600
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
601
|
+
let subscription = activeOrTrialingSubscription || incompleteSubscription;
|
|
602
|
+
if (incompleteSubscription && !activeOrTrialingSubscription) {
|
|
603
|
+
const updated = await ctx.context.adapter.update({
|
|
604
|
+
model: "subscription",
|
|
605
|
+
update: {
|
|
606
|
+
plan: plan.name.toLowerCase(),
|
|
607
|
+
seats: ctx.body.seats || 1,
|
|
608
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
609
|
+
},
|
|
610
|
+
where: [
|
|
611
|
+
{
|
|
612
|
+
field: "id",
|
|
613
|
+
value: incompleteSubscription.id
|
|
614
|
+
}
|
|
615
|
+
]
|
|
616
|
+
});
|
|
617
|
+
subscription = updated || incompleteSubscription;
|
|
618
|
+
}
|
|
619
|
+
if (!subscription) {
|
|
620
|
+
subscription = await ctx.context.adapter.create({
|
|
621
|
+
model: "subscription",
|
|
622
|
+
data: {
|
|
623
|
+
plan: plan.name.toLowerCase(),
|
|
624
|
+
stripeCustomerId: customerId,
|
|
625
|
+
status: "incomplete",
|
|
626
|
+
referenceId,
|
|
627
|
+
seats: ctx.body.seats || 1
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
602
631
|
if (!subscription) {
|
|
603
632
|
ctx.context.logger.error("Subscription ID not found");
|
|
604
633
|
throw new APIError("INTERNAL_SERVER_ERROR");
|
|
@@ -615,9 +644,8 @@ const stripe = (options) => {
|
|
|
615
644
|
ctx
|
|
616
645
|
);
|
|
617
646
|
const hasEverTrialed = subscriptions.some((s) => {
|
|
618
|
-
const samePlan = s.plan?.toLowerCase() === plan.name.toLowerCase();
|
|
619
647
|
const hadTrial = !!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
620
|
-
return
|
|
648
|
+
return hadTrial;
|
|
621
649
|
});
|
|
622
650
|
const freeTrial = !hasEverTrialed && plan.freeTrial ? { trial_period_days: plan.freeTrial.days } : void 0;
|
|
623
651
|
let priceIdToUse = void 0;
|
|
@@ -1081,9 +1109,10 @@ const stripe = (options) => {
|
|
|
1081
1109
|
status: "active"
|
|
1082
1110
|
}).then((res) => res.data[0]);
|
|
1083
1111
|
if (stripeSubscription) {
|
|
1084
|
-
const plan = await
|
|
1112
|
+
const plan = await getPlanByPriceInfo(
|
|
1085
1113
|
options,
|
|
1086
|
-
stripeSubscription.items.data[0]?.
|
|
1114
|
+
stripeSubscription.items.data[0]?.price.id,
|
|
1115
|
+
stripeSubscription.items.data[0]?.price.lookup_key
|
|
1087
1116
|
);
|
|
1088
1117
|
if (plan && subscription) {
|
|
1089
1118
|
await ctx.context.adapter.update({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/stripe",
|
|
3
3
|
"author": "Bereket Engida",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.10-beta.2",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -41,12 +41,13 @@
|
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"stripe": "^18",
|
|
44
|
-
"better-auth": "1.3.
|
|
44
|
+
"better-auth": "1.3.10-beta.2"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"better-call": "1.0.18",
|
|
48
48
|
"stripe": "^18.5.0",
|
|
49
|
-
"
|
|
49
|
+
"unbuild": "3.6.1",
|
|
50
|
+
"better-auth": "1.3.10-beta.2"
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
53
|
"test": "vitest",
|
package/src/hooks.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type GenericEndpointContext, logger } from "better-auth";
|
|
2
2
|
import type Stripe from "stripe";
|
|
3
3
|
import type { InputSubscription, StripeOptions, Subscription } from "./types";
|
|
4
|
-
import {
|
|
4
|
+
import { getPlanByPriceInfo } from "./utils";
|
|
5
5
|
|
|
6
6
|
export async function onCheckoutSessionCompleted(
|
|
7
7
|
ctx: GenericEndpointContext,
|
|
@@ -18,7 +18,12 @@ export async function onCheckoutSessionCompleted(
|
|
|
18
18
|
checkoutSession.subscription as string,
|
|
19
19
|
);
|
|
20
20
|
const priceId = subscription.items.data[0]?.price.id;
|
|
21
|
-
const
|
|
21
|
+
const priceLookupKey = subscription.items.data[0]?.price.lookup_key || null;
|
|
22
|
+
const plan = await getPlanByPriceInfo(
|
|
23
|
+
options,
|
|
24
|
+
priceId as string,
|
|
25
|
+
priceLookupKey,
|
|
26
|
+
);
|
|
22
27
|
if (plan) {
|
|
23
28
|
const referenceId =
|
|
24
29
|
checkoutSession?.client_reference_id ||
|
|
@@ -102,7 +107,9 @@ export async function onSubscriptionUpdated(
|
|
|
102
107
|
}
|
|
103
108
|
const subscriptionUpdated = event.data.object as Stripe.Subscription;
|
|
104
109
|
const priceId = subscriptionUpdated.items.data[0].price.id;
|
|
105
|
-
const
|
|
110
|
+
const priceLookupKey =
|
|
111
|
+
subscriptionUpdated.items.data[0].price.lookup_key || null;
|
|
112
|
+
const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
|
|
106
113
|
|
|
107
114
|
const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
|
|
108
115
|
const customerId = subscriptionUpdated.customer?.toString();
|
package/src/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
StripePlan,
|
|
25
25
|
Subscription,
|
|
26
26
|
} from "./types";
|
|
27
|
-
import { getPlanByName,
|
|
27
|
+
import { getPlanByName, getPlanByPriceInfo, getPlans } from "./utils";
|
|
28
28
|
import { getSchema } from "./schema";
|
|
29
29
|
|
|
30
30
|
const STRIPE_ERROR_CODES = {
|
|
@@ -354,15 +354,20 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
354
354
|
],
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
-
const
|
|
357
|
+
const activeOrTrialingSubscription = subscriptions.find(
|
|
358
358
|
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
359
359
|
);
|
|
360
360
|
|
|
361
|
+
// Also find any incomplete subscription that we can reuse
|
|
362
|
+
const incompleteSubscription = subscriptions.find(
|
|
363
|
+
(sub) => sub.status === "incomplete",
|
|
364
|
+
);
|
|
365
|
+
|
|
361
366
|
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
367
|
+
activeOrTrialingSubscription &&
|
|
368
|
+
activeOrTrialingSubscription.status === "active" &&
|
|
369
|
+
activeOrTrialingSubscription.plan === ctx.body.plan &&
|
|
370
|
+
activeOrTrialingSubscription.seats === (ctx.body.seats || 1)
|
|
366
371
|
) {
|
|
367
372
|
throw new APIError("BAD_REQUEST", {
|
|
368
373
|
message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
|
|
@@ -408,9 +413,32 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
408
413
|
});
|
|
409
414
|
}
|
|
410
415
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
416
|
+
let subscription: Subscription | undefined =
|
|
417
|
+
activeOrTrialingSubscription || incompleteSubscription;
|
|
418
|
+
|
|
419
|
+
if (incompleteSubscription && !activeOrTrialingSubscription) {
|
|
420
|
+
const updated = await ctx.context.adapter.update<InputSubscription>({
|
|
421
|
+
model: "subscription",
|
|
422
|
+
update: {
|
|
423
|
+
plan: plan.name.toLowerCase(),
|
|
424
|
+
seats: ctx.body.seats || 1,
|
|
425
|
+
updatedAt: new Date(),
|
|
426
|
+
},
|
|
427
|
+
where: [
|
|
428
|
+
{
|
|
429
|
+
field: "id",
|
|
430
|
+
value: incompleteSubscription.id,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
});
|
|
434
|
+
subscription = (updated as Subscription) || incompleteSubscription;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!subscription) {
|
|
438
|
+
subscription = await ctx.context.adapter.create<
|
|
439
|
+
InputSubscription,
|
|
440
|
+
Subscription
|
|
441
|
+
>({
|
|
414
442
|
model: "subscription",
|
|
415
443
|
data: {
|
|
416
444
|
plan: plan.name.toLowerCase(),
|
|
@@ -419,7 +447,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
419
447
|
referenceId,
|
|
420
448
|
seats: ctx.body.seats || 1,
|
|
421
449
|
},
|
|
422
|
-
})
|
|
450
|
+
});
|
|
451
|
+
}
|
|
423
452
|
|
|
424
453
|
if (!subscription) {
|
|
425
454
|
ctx.context.logger.error("Subscription ID not found");
|
|
@@ -439,10 +468,11 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
439
468
|
);
|
|
440
469
|
|
|
441
470
|
const hasEverTrialed = subscriptions.some((s) => {
|
|
442
|
-
|
|
471
|
+
// Check if user has ever had a trial for any plan (not just the same plan)
|
|
472
|
+
// This prevents users from getting multiple trials by switching plans
|
|
443
473
|
const hadTrial =
|
|
444
474
|
!!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
445
|
-
return
|
|
475
|
+
return hadTrial;
|
|
446
476
|
});
|
|
447
477
|
|
|
448
478
|
const freeTrial =
|
|
@@ -994,9 +1024,10 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
994
1024
|
.then((res) => res.data[0]);
|
|
995
1025
|
|
|
996
1026
|
if (stripeSubscription) {
|
|
997
|
-
const plan = await
|
|
1027
|
+
const plan = await getPlanByPriceInfo(
|
|
998
1028
|
options,
|
|
999
|
-
stripeSubscription.items.data[0]?.
|
|
1029
|
+
stripeSubscription.items.data[0]?.price.id,
|
|
1030
|
+
stripeSubscription.items.data[0]?.price.lookup_key,
|
|
1000
1031
|
);
|
|
1001
1032
|
|
|
1002
1033
|
if (plan && subscription) {
|
package/src/stripe.test.ts
CHANGED
|
@@ -1102,4 +1102,182 @@ describe("stripe", async () => {
|
|
|
1102
1102
|
});
|
|
1103
1103
|
expect(personalAfter?.status).toBe("active");
|
|
1104
1104
|
});
|
|
1105
|
+
|
|
1106
|
+
it("should prevent multiple free trials for the same user", async () => {
|
|
1107
|
+
// Create a user
|
|
1108
|
+
const userRes = await authClient.signUp.email(
|
|
1109
|
+
{ ...testUser, email: "trial-prevention@email.com" },
|
|
1110
|
+
{ throw: true },
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
const headers = new Headers();
|
|
1114
|
+
await authClient.signIn.email(
|
|
1115
|
+
{ ...testUser, email: "trial-prevention@email.com" },
|
|
1116
|
+
{
|
|
1117
|
+
throw: true,
|
|
1118
|
+
onSuccess: setCookieToHeader(headers),
|
|
1119
|
+
},
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
// First subscription with trial
|
|
1123
|
+
const firstUpgradeRes = await authClient.subscription.upgrade({
|
|
1124
|
+
plan: "starter",
|
|
1125
|
+
fetchOptions: { headers },
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
expect(firstUpgradeRes.data?.url).toBeDefined();
|
|
1129
|
+
|
|
1130
|
+
// Simulate the subscription being created with trial data
|
|
1131
|
+
await ctx.adapter.update({
|
|
1132
|
+
model: "subscription",
|
|
1133
|
+
update: {
|
|
1134
|
+
status: "trialing",
|
|
1135
|
+
trialStart: new Date(),
|
|
1136
|
+
trialEnd: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
|
|
1137
|
+
},
|
|
1138
|
+
where: [
|
|
1139
|
+
{
|
|
1140
|
+
field: "referenceId",
|
|
1141
|
+
value: userRes.user.id,
|
|
1142
|
+
},
|
|
1143
|
+
],
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
// Cancel the subscription
|
|
1147
|
+
await ctx.adapter.update({
|
|
1148
|
+
model: "subscription",
|
|
1149
|
+
update: {
|
|
1150
|
+
status: "canceled",
|
|
1151
|
+
},
|
|
1152
|
+
where: [
|
|
1153
|
+
{
|
|
1154
|
+
field: "referenceId",
|
|
1155
|
+
value: userRes.user.id,
|
|
1156
|
+
},
|
|
1157
|
+
],
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
// Try to subscribe again - should NOT get a trial
|
|
1161
|
+
const secondUpgradeRes = await authClient.subscription.upgrade({
|
|
1162
|
+
plan: "starter",
|
|
1163
|
+
fetchOptions: { headers },
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
expect(secondUpgradeRes.data?.url).toBeDefined();
|
|
1167
|
+
|
|
1168
|
+
// Verify that the checkout session was created without trial_period_days
|
|
1169
|
+
// We can't directly test the Stripe session, but we can verify the logic
|
|
1170
|
+
// by checking that the user has trial history
|
|
1171
|
+
const subscriptions = (await ctx.adapter.findMany({
|
|
1172
|
+
model: "subscription",
|
|
1173
|
+
where: [
|
|
1174
|
+
{
|
|
1175
|
+
field: "referenceId",
|
|
1176
|
+
value: userRes.user.id,
|
|
1177
|
+
},
|
|
1178
|
+
],
|
|
1179
|
+
})) as Subscription[];
|
|
1180
|
+
|
|
1181
|
+
// Should have 2 subscriptions (first canceled, second new)
|
|
1182
|
+
expect(subscriptions).toHaveLength(2);
|
|
1183
|
+
|
|
1184
|
+
// At least one should have trial data
|
|
1185
|
+
const hasTrialData = subscriptions.some(
|
|
1186
|
+
(s: Subscription) => s.trialStart || s.trialEnd,
|
|
1187
|
+
);
|
|
1188
|
+
expect(hasTrialData).toBe(true);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
it("should prevent multiple free trials across different plans", async () => {
|
|
1192
|
+
// Create a user
|
|
1193
|
+
const userRes = await authClient.signUp.email(
|
|
1194
|
+
{ ...testUser, email: "cross-plan-trial@email.com" },
|
|
1195
|
+
{ throw: true },
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
const headers = new Headers();
|
|
1199
|
+
await authClient.signIn.email(
|
|
1200
|
+
{ ...testUser, email: "cross-plan-trial@email.com" },
|
|
1201
|
+
{
|
|
1202
|
+
throw: true,
|
|
1203
|
+
onSuccess: setCookieToHeader(headers),
|
|
1204
|
+
},
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
// First subscription with trial on starter plan
|
|
1208
|
+
const firstUpgradeRes = await authClient.subscription.upgrade({
|
|
1209
|
+
plan: "starter",
|
|
1210
|
+
fetchOptions: { headers },
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
expect(firstUpgradeRes.data?.url).toBeDefined();
|
|
1214
|
+
|
|
1215
|
+
// Simulate the subscription being created with trial data
|
|
1216
|
+
await ctx.adapter.update({
|
|
1217
|
+
model: "subscription",
|
|
1218
|
+
update: {
|
|
1219
|
+
status: "trialing",
|
|
1220
|
+
trialStart: new Date(),
|
|
1221
|
+
trialEnd: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
1222
|
+
},
|
|
1223
|
+
where: [
|
|
1224
|
+
{
|
|
1225
|
+
field: "referenceId",
|
|
1226
|
+
value: userRes.user.id,
|
|
1227
|
+
},
|
|
1228
|
+
],
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
// Cancel the subscription
|
|
1232
|
+
await ctx.adapter.update({
|
|
1233
|
+
model: "subscription",
|
|
1234
|
+
update: {
|
|
1235
|
+
status: "canceled",
|
|
1236
|
+
},
|
|
1237
|
+
where: [
|
|
1238
|
+
{
|
|
1239
|
+
field: "referenceId",
|
|
1240
|
+
value: userRes.user.id,
|
|
1241
|
+
},
|
|
1242
|
+
],
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
// Try to subscribe to a different plan - should NOT get a trial
|
|
1246
|
+
const secondUpgradeRes = await authClient.subscription.upgrade({
|
|
1247
|
+
plan: "premium",
|
|
1248
|
+
fetchOptions: { headers },
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
expect(secondUpgradeRes.data?.url).toBeDefined();
|
|
1252
|
+
|
|
1253
|
+
// Verify that the user has trial history from the first plan
|
|
1254
|
+
const subscriptions = (await ctx.adapter.findMany({
|
|
1255
|
+
model: "subscription",
|
|
1256
|
+
where: [
|
|
1257
|
+
{
|
|
1258
|
+
field: "referenceId",
|
|
1259
|
+
value: userRes.user.id,
|
|
1260
|
+
},
|
|
1261
|
+
],
|
|
1262
|
+
})) as Subscription[];
|
|
1263
|
+
|
|
1264
|
+
// Should have at least 1 subscription (the starter with trial data)
|
|
1265
|
+
expect(subscriptions.length).toBeGreaterThanOrEqual(1);
|
|
1266
|
+
|
|
1267
|
+
// The starter subscription should have trial data
|
|
1268
|
+
const starterSub = subscriptions.find(
|
|
1269
|
+
(s: Subscription) => s.plan === "starter",
|
|
1270
|
+
) as Subscription | undefined;
|
|
1271
|
+
expect(starterSub?.trialStart).toBeDefined();
|
|
1272
|
+
expect(starterSub?.trialEnd).toBeDefined();
|
|
1273
|
+
|
|
1274
|
+
// Verify that the trial eligibility logic is working by checking
|
|
1275
|
+
// that the user has ever had a trial (which should prevent future trials)
|
|
1276
|
+
const hasEverTrialed = subscriptions.some((s: Subscription) => {
|
|
1277
|
+
const hadTrial =
|
|
1278
|
+
!!(s.trialStart || s.trialEnd) || s.status === "trialing";
|
|
1279
|
+
return hadTrial;
|
|
1280
|
+
});
|
|
1281
|
+
expect(hasEverTrialed).toBe(true);
|
|
1282
|
+
});
|
|
1105
1283
|
});
|
package/src/utils.ts
CHANGED
|
@@ -6,14 +6,19 @@ export async function getPlans(options: StripeOptions) {
|
|
|
6
6
|
: options.subscription?.plans;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export async function
|
|
9
|
+
export async function getPlanByPriceInfo(
|
|
10
10
|
options: StripeOptions,
|
|
11
11
|
priceId: string,
|
|
12
|
+
priceLookupKey: string | null,
|
|
12
13
|
) {
|
|
13
14
|
return await getPlans(options).then((res) =>
|
|
14
15
|
res?.find(
|
|
15
16
|
(plan) =>
|
|
16
|
-
plan.priceId === priceId ||
|
|
17
|
+
plan.priceId === priceId ||
|
|
18
|
+
plan.annualDiscountPriceId === priceId ||
|
|
19
|
+
(priceLookupKey &&
|
|
20
|
+
(plan.lookupKey === priceLookupKey ||
|
|
21
|
+
plan.annualDiscountLookupKey === priceLookupKey)),
|
|
17
22
|
),
|
|
18
23
|
);
|
|
19
24
|
}
|