@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.
@@ -1,17 +1,17 @@
1
1
 
2
- > @better-auth/stripe@1.3.9 build /home/runner/work/better-auth/better-auth/packages/stripe
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: 44.2 kB, chunk size: 44.2 kB, exports: stripe)
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: 43.4 kB, chunk size: 43.4 kB, exports: stripe)
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): 224 kB
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 getPlanByPriceId(options, priceId) {
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 plan = await getPlanByPriceId(options, priceId);
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 plan = await getPlanByPriceId(options, priceId);
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 existingSubscription = subscriptions.find(
572
+ const activeOrTrialingSubscription = subscriptions.find(
567
573
  (sub) => sub.status === "active" || sub.status === "trialing"
568
574
  );
569
- if (existingSubscription && existingSubscription.status === "active" && existingSubscription.plan === ctx.body.plan && existingSubscription.seats === (ctx.body.seats || 1)) {
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
- const subscription = existingSubscription || await ctx.context.adapter.create({
609
- model: "subscription",
610
- data: {
611
- plan: plan.name.toLowerCase(),
612
- stripeCustomerId: customerId,
613
- status: "incomplete",
614
- referenceId,
615
- seats: ctx.body.seats || 1
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 samePlan && hadTrial;
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 getPlanByPriceId(
1128
+ const plan = await getPlanByPriceInfo(
1101
1129
  options,
1102
- stripeSubscription.items.data[0]?.plan.id
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 getPlanByPriceId(options, priceId) {
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 plan = await getPlanByPriceId(options, priceId);
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 plan = await getPlanByPriceId(options, priceId);
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 existingSubscription = subscriptions.find(
556
+ const activeOrTrialingSubscription = subscriptions.find(
551
557
  (sub) => sub.status === "active" || sub.status === "trialing"
552
558
  );
553
- if (existingSubscription && existingSubscription.status === "active" && existingSubscription.plan === ctx.body.plan && existingSubscription.seats === (ctx.body.seats || 1)) {
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
- const subscription = existingSubscription || await ctx.context.adapter.create({
593
- model: "subscription",
594
- data: {
595
- plan: plan.name.toLowerCase(),
596
- stripeCustomerId: customerId,
597
- status: "incomplete",
598
- referenceId,
599
- seats: ctx.body.seats || 1
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 samePlan && hadTrial;
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 getPlanByPriceId(
1112
+ const plan = await getPlanByPriceInfo(
1085
1113
  options,
1086
- stripeSubscription.items.data[0]?.plan.id
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.9",
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.9"
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
- "better-auth": "1.3.9"
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 { getPlanByPriceId } from "./utils";
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 plan = await getPlanByPriceId(options, priceId as string);
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 plan = await getPlanByPriceId(options, priceId);
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, getPlanByPriceId, getPlans } from "./utils";
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 existingSubscription = subscriptions.find(
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
- existingSubscription &&
363
- existingSubscription.status === "active" &&
364
- existingSubscription.plan === ctx.body.plan &&
365
- existingSubscription.seats === (ctx.body.seats || 1)
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
- const subscription =
412
- existingSubscription ||
413
- (await ctx.context.adapter.create<InputSubscription, Subscription>({
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
- const samePlan = s.plan?.toLowerCase() === plan.name.toLowerCase();
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 samePlan && hadTrial;
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 getPlanByPriceId(
1027
+ const plan = await getPlanByPriceInfo(
998
1028
  options,
999
- stripeSubscription.items.data[0]?.plan.id,
1029
+ stripeSubscription.items.data[0]?.price.id,
1030
+ stripeSubscription.items.data[0]?.price.lookup_key,
1000
1031
  );
1001
1032
 
1002
1033
  if (plan && subscription) {
@@ -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 getPlanByPriceId(
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 || plan.annualDiscountPriceId === 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
  }
package/vitest.config.ts CHANGED
@@ -5,6 +5,5 @@ export default defineConfig({
5
5
  test: {
6
6
  clearMocks: true,
7
7
  globals: true,
8
- setupFiles: ["dotenv/config"],
9
8
  },
10
9
  });