@better-auth/stripe 1.3.24 → 1.3.26
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 +118 -18
- package/dist/index.d.cts +12 -4
- package/dist/index.d.mts +12 -4
- package/dist/index.d.ts +12 -4
- package/dist/index.mjs +118 -18
- package/package.json +4 -3
- package/src/index.ts +155 -26
- package/src/stripe.test.ts +459 -0
- package/src/types.ts +2 -5
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
|
|
2
|
-
> @better-auth/stripe@1.3.
|
|
2
|
+
> @better-auth/stripe@1.3.26 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: 49 kB, chunk size: 49 kB, exports: stripe)
|
|
9
9
|
|
|
10
10
|
[log] dist/client.cjs (total size: 270 B, chunk size: 270 B, exports: stripeClient)
|
|
11
11
|
|
|
12
|
-
[log] dist/index.mjs (total size:
|
|
12
|
+
[log] dist/index.mjs (total size: 48.2 kB, chunk size: 48.2 kB, exports: stripe)
|
|
13
13
|
|
|
14
14
|
[log] dist/client.mjs (total size: 243 B, chunk size: 243 B, exports: stripeClient)
|
|
15
15
|
|
|
16
|
-
Σ Total dist size (byte size):
|
|
16
|
+
Σ Total dist size (byte size): 236 kB
|
|
17
17
|
[log]
|
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const plugins = require('better-auth/plugins');
|
|
|
5
5
|
const z = require('zod/v4');
|
|
6
6
|
const api = require('better-auth/api');
|
|
7
7
|
const db = require('better-auth/db');
|
|
8
|
+
const defu = require('defu');
|
|
8
9
|
|
|
9
10
|
function _interopNamespaceCompat(e) {
|
|
10
11
|
if (e && typeof e === 'object' && 'default' in e) return e;
|
|
@@ -550,16 +551,6 @@ const stripe = (options) => {
|
|
|
550
551
|
});
|
|
551
552
|
}
|
|
552
553
|
}
|
|
553
|
-
const activeSubscriptions = await client.subscriptions.list({
|
|
554
|
-
customer: customerId
|
|
555
|
-
}).then(
|
|
556
|
-
(res) => res.data.filter(
|
|
557
|
-
(sub) => sub.status === "active" || sub.status === "trialing"
|
|
558
|
-
)
|
|
559
|
-
);
|
|
560
|
-
const activeSubscription = activeSubscriptions.find(
|
|
561
|
-
(sub) => subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId ? sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId : false
|
|
562
|
-
);
|
|
563
554
|
const subscriptions = subscriptionToUpdate ? [subscriptionToUpdate] : await ctx.context.adapter.findMany({
|
|
564
555
|
model: "subscription",
|
|
565
556
|
where: [
|
|
@@ -572,6 +563,22 @@ const stripe = (options) => {
|
|
|
572
563
|
const activeOrTrialingSubscription = subscriptions.find(
|
|
573
564
|
(sub) => sub.status === "active" || sub.status === "trialing"
|
|
574
565
|
);
|
|
566
|
+
const activeSubscriptions = await client.subscriptions.list({
|
|
567
|
+
customer: customerId
|
|
568
|
+
}).then(
|
|
569
|
+
(res) => res.data.filter(
|
|
570
|
+
(sub) => sub.status === "active" || sub.status === "trialing"
|
|
571
|
+
)
|
|
572
|
+
);
|
|
573
|
+
const activeSubscription = activeSubscriptions.find((sub) => {
|
|
574
|
+
if (subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId) {
|
|
575
|
+
return sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId;
|
|
576
|
+
}
|
|
577
|
+
if (activeOrTrialingSubscription?.stripeSubscriptionId) {
|
|
578
|
+
return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
});
|
|
575
582
|
const incompleteSubscription = subscriptions.find(
|
|
576
583
|
(sub) => sub.status === "incomplete"
|
|
577
584
|
);
|
|
@@ -581,6 +588,54 @@ const stripe = (options) => {
|
|
|
581
588
|
});
|
|
582
589
|
}
|
|
583
590
|
if (activeSubscription && customerId) {
|
|
591
|
+
let dbSubscription = await ctx.context.adapter.findOne({
|
|
592
|
+
model: "subscription",
|
|
593
|
+
where: [
|
|
594
|
+
{
|
|
595
|
+
field: "stripeSubscriptionId",
|
|
596
|
+
value: activeSubscription.id
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
});
|
|
600
|
+
if (!dbSubscription && activeOrTrialingSubscription) {
|
|
601
|
+
await ctx.context.adapter.update({
|
|
602
|
+
model: "subscription",
|
|
603
|
+
update: {
|
|
604
|
+
stripeSubscriptionId: activeSubscription.id,
|
|
605
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
606
|
+
},
|
|
607
|
+
where: [
|
|
608
|
+
{
|
|
609
|
+
field: "id",
|
|
610
|
+
value: activeOrTrialingSubscription.id
|
|
611
|
+
}
|
|
612
|
+
]
|
|
613
|
+
});
|
|
614
|
+
dbSubscription = activeOrTrialingSubscription;
|
|
615
|
+
}
|
|
616
|
+
let priceIdToUse2 = void 0;
|
|
617
|
+
if (ctx.body.annual) {
|
|
618
|
+
priceIdToUse2 = plan.annualDiscountPriceId;
|
|
619
|
+
if (!priceIdToUse2 && plan.annualDiscountLookupKey) {
|
|
620
|
+
priceIdToUse2 = await resolvePriceIdFromLookupKey(
|
|
621
|
+
client,
|
|
622
|
+
plan.annualDiscountLookupKey
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
priceIdToUse2 = plan.priceId;
|
|
627
|
+
if (!priceIdToUse2 && plan.lookupKey) {
|
|
628
|
+
priceIdToUse2 = await resolvePriceIdFromLookupKey(
|
|
629
|
+
client,
|
|
630
|
+
plan.lookupKey
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (!priceIdToUse2) {
|
|
635
|
+
throw ctx.error("BAD_REQUEST", {
|
|
636
|
+
message: "Price ID not found for the selected plan"
|
|
637
|
+
});
|
|
638
|
+
}
|
|
584
639
|
const { url } = await client.billingPortal.sessions.create({
|
|
585
640
|
customer: customerId,
|
|
586
641
|
return_url: getUrl(ctx, ctx.body.returnUrl || "/"),
|
|
@@ -598,7 +653,7 @@ const stripe = (options) => {
|
|
|
598
653
|
{
|
|
599
654
|
id: activeSubscription.items.data[0]?.id,
|
|
600
655
|
quantity: ctx.body.seats || 1,
|
|
601
|
-
price:
|
|
656
|
+
price: priceIdToUse2
|
|
602
657
|
}
|
|
603
658
|
]
|
|
604
659
|
}
|
|
@@ -1314,13 +1369,24 @@ const stripe = (options) => {
|
|
|
1314
1369
|
create: {
|
|
1315
1370
|
async after(user, ctx2) {
|
|
1316
1371
|
if (ctx2 && options.createCustomerOnSignUp) {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
}
|
|
1372
|
+
let extraCreateParams = {};
|
|
1373
|
+
if (options.getCustomerCreateParams) {
|
|
1374
|
+
extraCreateParams = await options.getCustomerCreateParams(
|
|
1375
|
+
user,
|
|
1376
|
+
ctx2
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
const params = defu.defu(
|
|
1380
|
+
{
|
|
1381
|
+
email: user.email,
|
|
1382
|
+
name: user.name,
|
|
1383
|
+
metadata: {
|
|
1384
|
+
userId: user.id
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
extraCreateParams
|
|
1388
|
+
);
|
|
1389
|
+
const stripeCustomer = await client.customers.create(params);
|
|
1324
1390
|
await ctx2.context.internalAdapter.updateUser(user.id, {
|
|
1325
1391
|
stripeCustomerId: stripeCustomer.id
|
|
1326
1392
|
});
|
|
@@ -1336,6 +1402,40 @@ const stripe = (options) => {
|
|
|
1336
1402
|
);
|
|
1337
1403
|
}
|
|
1338
1404
|
}
|
|
1405
|
+
},
|
|
1406
|
+
update: {
|
|
1407
|
+
async after(user, ctx2) {
|
|
1408
|
+
if (!ctx2) return;
|
|
1409
|
+
try {
|
|
1410
|
+
const userWithStripe = user;
|
|
1411
|
+
if (!userWithStripe.stripeCustomerId) return;
|
|
1412
|
+
const stripeCustomer = await client.customers.retrieve(
|
|
1413
|
+
userWithStripe.stripeCustomerId
|
|
1414
|
+
);
|
|
1415
|
+
if (stripeCustomer.deleted) {
|
|
1416
|
+
ctx2.context.logger.warn(
|
|
1417
|
+
`Stripe customer ${userWithStripe.stripeCustomerId} was deleted, cannot update email`
|
|
1418
|
+
);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (stripeCustomer.email !== user.email) {
|
|
1422
|
+
await client.customers.update(
|
|
1423
|
+
userWithStripe.stripeCustomerId,
|
|
1424
|
+
{
|
|
1425
|
+
email: user.email
|
|
1426
|
+
}
|
|
1427
|
+
);
|
|
1428
|
+
ctx2.context.logger.info(
|
|
1429
|
+
`Updated Stripe customer email from ${stripeCustomer.email} to ${user.email}`
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
} catch (e) {
|
|
1433
|
+
ctx2.context.logger.error(
|
|
1434
|
+
`Failed to sync email to Stripe customer: ${e.message}`,
|
|
1435
|
+
e
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1339
1439
|
}
|
|
1340
1440
|
}
|
|
1341
1441
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -240,10 +240,7 @@ interface StripeOptions {
|
|
|
240
240
|
* @param data - data containing user and session
|
|
241
241
|
* @returns
|
|
242
242
|
*/
|
|
243
|
-
getCustomerCreateParams?: (
|
|
244
|
-
user: User;
|
|
245
|
-
session: Session;
|
|
246
|
-
}, ctx: GenericEndpointContext) => Promise<{}>;
|
|
243
|
+
getCustomerCreateParams?: (user: User, ctx: GenericEndpointContext) => Promise<Partial<Stripe.CustomerCreateParams>>;
|
|
247
244
|
/**
|
|
248
245
|
* Subscriptions
|
|
249
246
|
*/
|
|
@@ -1081,6 +1078,17 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
|
|
|
1081
1078
|
image?: string | null | undefined;
|
|
1082
1079
|
} & Record<string, unknown>, ctx: GenericEndpointContext | undefined): Promise<void>;
|
|
1083
1080
|
};
|
|
1081
|
+
update: {
|
|
1082
|
+
after(user: {
|
|
1083
|
+
id: string;
|
|
1084
|
+
createdAt: Date;
|
|
1085
|
+
updatedAt: Date;
|
|
1086
|
+
email: string;
|
|
1087
|
+
emailVerified: boolean;
|
|
1088
|
+
name: string;
|
|
1089
|
+
image?: string | null | undefined;
|
|
1090
|
+
} & Record<string, unknown>, ctx: GenericEndpointContext | undefined): Promise<void>;
|
|
1091
|
+
};
|
|
1084
1092
|
};
|
|
1085
1093
|
};
|
|
1086
1094
|
};
|
package/dist/index.d.mts
CHANGED
|
@@ -240,10 +240,7 @@ interface StripeOptions {
|
|
|
240
240
|
* @param data - data containing user and session
|
|
241
241
|
* @returns
|
|
242
242
|
*/
|
|
243
|
-
getCustomerCreateParams?: (
|
|
244
|
-
user: User;
|
|
245
|
-
session: Session;
|
|
246
|
-
}, ctx: GenericEndpointContext) => Promise<{}>;
|
|
243
|
+
getCustomerCreateParams?: (user: User, ctx: GenericEndpointContext) => Promise<Partial<Stripe.CustomerCreateParams>>;
|
|
247
244
|
/**
|
|
248
245
|
* Subscriptions
|
|
249
246
|
*/
|
|
@@ -1081,6 +1078,17 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
|
|
|
1081
1078
|
image?: string | null | undefined;
|
|
1082
1079
|
} & Record<string, unknown>, ctx: GenericEndpointContext | undefined): Promise<void>;
|
|
1083
1080
|
};
|
|
1081
|
+
update: {
|
|
1082
|
+
after(user: {
|
|
1083
|
+
id: string;
|
|
1084
|
+
createdAt: Date;
|
|
1085
|
+
updatedAt: Date;
|
|
1086
|
+
email: string;
|
|
1087
|
+
emailVerified: boolean;
|
|
1088
|
+
name: string;
|
|
1089
|
+
image?: string | null | undefined;
|
|
1090
|
+
} & Record<string, unknown>, ctx: GenericEndpointContext | undefined): Promise<void>;
|
|
1091
|
+
};
|
|
1084
1092
|
};
|
|
1085
1093
|
};
|
|
1086
1094
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -240,10 +240,7 @@ interface StripeOptions {
|
|
|
240
240
|
* @param data - data containing user and session
|
|
241
241
|
* @returns
|
|
242
242
|
*/
|
|
243
|
-
getCustomerCreateParams?: (
|
|
244
|
-
user: User;
|
|
245
|
-
session: Session;
|
|
246
|
-
}, ctx: GenericEndpointContext) => Promise<{}>;
|
|
243
|
+
getCustomerCreateParams?: (user: User, ctx: GenericEndpointContext) => Promise<Partial<Stripe.CustomerCreateParams>>;
|
|
247
244
|
/**
|
|
248
245
|
* Subscriptions
|
|
249
246
|
*/
|
|
@@ -1081,6 +1078,17 @@ declare const stripe: <O extends StripeOptions>(options: O) => {
|
|
|
1081
1078
|
image?: string | null | undefined;
|
|
1082
1079
|
} & Record<string, unknown>, ctx: GenericEndpointContext | undefined): Promise<void>;
|
|
1083
1080
|
};
|
|
1081
|
+
update: {
|
|
1082
|
+
after(user: {
|
|
1083
|
+
id: string;
|
|
1084
|
+
createdAt: Date;
|
|
1085
|
+
updatedAt: Date;
|
|
1086
|
+
email: string;
|
|
1087
|
+
emailVerified: boolean;
|
|
1088
|
+
name: string;
|
|
1089
|
+
image?: string | null | undefined;
|
|
1090
|
+
} & Record<string, unknown>, ctx: GenericEndpointContext | undefined): Promise<void>;
|
|
1091
|
+
};
|
|
1084
1092
|
};
|
|
1085
1093
|
};
|
|
1086
1094
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
|
|
|
3
3
|
import * as z from 'zod/v4';
|
|
4
4
|
import { sessionMiddleware, originCheck, APIError, getSessionFromCtx } from 'better-auth/api';
|
|
5
5
|
import { mergeSchema } from 'better-auth/db';
|
|
6
|
+
import { defu } from 'defu';
|
|
6
7
|
|
|
7
8
|
async function getPlans(options) {
|
|
8
9
|
return typeof options?.subscription?.plans === "function" ? await options.subscription?.plans() : options.subscription?.plans;
|
|
@@ -534,16 +535,6 @@ const stripe = (options) => {
|
|
|
534
535
|
});
|
|
535
536
|
}
|
|
536
537
|
}
|
|
537
|
-
const activeSubscriptions = await client.subscriptions.list({
|
|
538
|
-
customer: customerId
|
|
539
|
-
}).then(
|
|
540
|
-
(res) => res.data.filter(
|
|
541
|
-
(sub) => sub.status === "active" || sub.status === "trialing"
|
|
542
|
-
)
|
|
543
|
-
);
|
|
544
|
-
const activeSubscription = activeSubscriptions.find(
|
|
545
|
-
(sub) => subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId ? sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId : false
|
|
546
|
-
);
|
|
547
538
|
const subscriptions = subscriptionToUpdate ? [subscriptionToUpdate] : await ctx.context.adapter.findMany({
|
|
548
539
|
model: "subscription",
|
|
549
540
|
where: [
|
|
@@ -556,6 +547,22 @@ const stripe = (options) => {
|
|
|
556
547
|
const activeOrTrialingSubscription = subscriptions.find(
|
|
557
548
|
(sub) => sub.status === "active" || sub.status === "trialing"
|
|
558
549
|
);
|
|
550
|
+
const activeSubscriptions = await client.subscriptions.list({
|
|
551
|
+
customer: customerId
|
|
552
|
+
}).then(
|
|
553
|
+
(res) => res.data.filter(
|
|
554
|
+
(sub) => sub.status === "active" || sub.status === "trialing"
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
const activeSubscription = activeSubscriptions.find((sub) => {
|
|
558
|
+
if (subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId) {
|
|
559
|
+
return sub.id === subscriptionToUpdate?.stripeSubscriptionId || sub.id === ctx.body.subscriptionId;
|
|
560
|
+
}
|
|
561
|
+
if (activeOrTrialingSubscription?.stripeSubscriptionId) {
|
|
562
|
+
return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
|
|
563
|
+
}
|
|
564
|
+
return false;
|
|
565
|
+
});
|
|
559
566
|
const incompleteSubscription = subscriptions.find(
|
|
560
567
|
(sub) => sub.status === "incomplete"
|
|
561
568
|
);
|
|
@@ -565,6 +572,54 @@ const stripe = (options) => {
|
|
|
565
572
|
});
|
|
566
573
|
}
|
|
567
574
|
if (activeSubscription && customerId) {
|
|
575
|
+
let dbSubscription = await ctx.context.adapter.findOne({
|
|
576
|
+
model: "subscription",
|
|
577
|
+
where: [
|
|
578
|
+
{
|
|
579
|
+
field: "stripeSubscriptionId",
|
|
580
|
+
value: activeSubscription.id
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
});
|
|
584
|
+
if (!dbSubscription && activeOrTrialingSubscription) {
|
|
585
|
+
await ctx.context.adapter.update({
|
|
586
|
+
model: "subscription",
|
|
587
|
+
update: {
|
|
588
|
+
stripeSubscriptionId: activeSubscription.id,
|
|
589
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
590
|
+
},
|
|
591
|
+
where: [
|
|
592
|
+
{
|
|
593
|
+
field: "id",
|
|
594
|
+
value: activeOrTrialingSubscription.id
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
});
|
|
598
|
+
dbSubscription = activeOrTrialingSubscription;
|
|
599
|
+
}
|
|
600
|
+
let priceIdToUse2 = void 0;
|
|
601
|
+
if (ctx.body.annual) {
|
|
602
|
+
priceIdToUse2 = plan.annualDiscountPriceId;
|
|
603
|
+
if (!priceIdToUse2 && plan.annualDiscountLookupKey) {
|
|
604
|
+
priceIdToUse2 = await resolvePriceIdFromLookupKey(
|
|
605
|
+
client,
|
|
606
|
+
plan.annualDiscountLookupKey
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
priceIdToUse2 = plan.priceId;
|
|
611
|
+
if (!priceIdToUse2 && plan.lookupKey) {
|
|
612
|
+
priceIdToUse2 = await resolvePriceIdFromLookupKey(
|
|
613
|
+
client,
|
|
614
|
+
plan.lookupKey
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (!priceIdToUse2) {
|
|
619
|
+
throw ctx.error("BAD_REQUEST", {
|
|
620
|
+
message: "Price ID not found for the selected plan"
|
|
621
|
+
});
|
|
622
|
+
}
|
|
568
623
|
const { url } = await client.billingPortal.sessions.create({
|
|
569
624
|
customer: customerId,
|
|
570
625
|
return_url: getUrl(ctx, ctx.body.returnUrl || "/"),
|
|
@@ -582,7 +637,7 @@ const stripe = (options) => {
|
|
|
582
637
|
{
|
|
583
638
|
id: activeSubscription.items.data[0]?.id,
|
|
584
639
|
quantity: ctx.body.seats || 1,
|
|
585
|
-
price:
|
|
640
|
+
price: priceIdToUse2
|
|
586
641
|
}
|
|
587
642
|
]
|
|
588
643
|
}
|
|
@@ -1298,13 +1353,24 @@ const stripe = (options) => {
|
|
|
1298
1353
|
create: {
|
|
1299
1354
|
async after(user, ctx2) {
|
|
1300
1355
|
if (ctx2 && options.createCustomerOnSignUp) {
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1356
|
+
let extraCreateParams = {};
|
|
1357
|
+
if (options.getCustomerCreateParams) {
|
|
1358
|
+
extraCreateParams = await options.getCustomerCreateParams(
|
|
1359
|
+
user,
|
|
1360
|
+
ctx2
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
const params = defu(
|
|
1364
|
+
{
|
|
1365
|
+
email: user.email,
|
|
1366
|
+
name: user.name,
|
|
1367
|
+
metadata: {
|
|
1368
|
+
userId: user.id
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
extraCreateParams
|
|
1372
|
+
);
|
|
1373
|
+
const stripeCustomer = await client.customers.create(params);
|
|
1308
1374
|
await ctx2.context.internalAdapter.updateUser(user.id, {
|
|
1309
1375
|
stripeCustomerId: stripeCustomer.id
|
|
1310
1376
|
});
|
|
@@ -1320,6 +1386,40 @@ const stripe = (options) => {
|
|
|
1320
1386
|
);
|
|
1321
1387
|
}
|
|
1322
1388
|
}
|
|
1389
|
+
},
|
|
1390
|
+
update: {
|
|
1391
|
+
async after(user, ctx2) {
|
|
1392
|
+
if (!ctx2) return;
|
|
1393
|
+
try {
|
|
1394
|
+
const userWithStripe = user;
|
|
1395
|
+
if (!userWithStripe.stripeCustomerId) return;
|
|
1396
|
+
const stripeCustomer = await client.customers.retrieve(
|
|
1397
|
+
userWithStripe.stripeCustomerId
|
|
1398
|
+
);
|
|
1399
|
+
if (stripeCustomer.deleted) {
|
|
1400
|
+
ctx2.context.logger.warn(
|
|
1401
|
+
`Stripe customer ${userWithStripe.stripeCustomerId} was deleted, cannot update email`
|
|
1402
|
+
);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (stripeCustomer.email !== user.email) {
|
|
1406
|
+
await client.customers.update(
|
|
1407
|
+
userWithStripe.stripeCustomerId,
|
|
1408
|
+
{
|
|
1409
|
+
email: user.email
|
|
1410
|
+
}
|
|
1411
|
+
);
|
|
1412
|
+
ctx2.context.logger.info(
|
|
1413
|
+
`Updated Stripe customer email from ${stripeCustomer.email} to ${user.email}`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
ctx2.context.logger.error(
|
|
1418
|
+
`Failed to sync email to Stripe customer: ${e.message}`,
|
|
1419
|
+
e
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1323
1423
|
}
|
|
1324
1424
|
}
|
|
1325
1425
|
}
|
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.26",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -37,17 +37,18 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"defu": "^6.1.4",
|
|
40
41
|
"zod": "^4.1.5"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"stripe": "^18",
|
|
44
|
-
"better-auth": "1.3.
|
|
45
|
+
"better-auth": "1.3.26"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"better-call": "1.0.19",
|
|
48
49
|
"stripe": "^18.5.0",
|
|
49
50
|
"unbuild": "3.6.1",
|
|
50
|
-
"better-auth": "1.3.
|
|
51
|
+
"better-auth": "1.3.26"
|
|
51
52
|
},
|
|
52
53
|
"scripts": {
|
|
53
54
|
"test": "vitest",
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
} from "./types";
|
|
27
27
|
import { getPlanByName, getPlanByPriceInfo, getPlans } from "./utils";
|
|
28
28
|
import { getSchema } from "./schema";
|
|
29
|
+
import { defu } from "defu";
|
|
29
30
|
|
|
30
31
|
const STRIPE_ERROR_CODES = {
|
|
31
32
|
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
|
|
@@ -325,23 +326,6 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
const activeSubscriptions = await client.subscriptions
|
|
329
|
-
.list({
|
|
330
|
-
customer: customerId,
|
|
331
|
-
})
|
|
332
|
-
.then((res) =>
|
|
333
|
-
res.data.filter(
|
|
334
|
-
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
335
|
-
),
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
const activeSubscription = activeSubscriptions.find((sub) =>
|
|
339
|
-
subscriptionToUpdate?.stripeSubscriptionId || ctx.body.subscriptionId
|
|
340
|
-
? sub.id === subscriptionToUpdate?.stripeSubscriptionId ||
|
|
341
|
-
sub.id === ctx.body.subscriptionId
|
|
342
|
-
: false,
|
|
343
|
-
);
|
|
344
|
-
|
|
345
329
|
const subscriptions = subscriptionToUpdate
|
|
346
330
|
? [subscriptionToUpdate]
|
|
347
331
|
: await ctx.context.adapter.findMany<Subscription>({
|
|
@@ -358,6 +342,34 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
358
342
|
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
359
343
|
);
|
|
360
344
|
|
|
345
|
+
const activeSubscriptions = await client.subscriptions
|
|
346
|
+
.list({
|
|
347
|
+
customer: customerId,
|
|
348
|
+
})
|
|
349
|
+
.then((res) =>
|
|
350
|
+
res.data.filter(
|
|
351
|
+
(sub) => sub.status === "active" || sub.status === "trialing",
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const activeSubscription = activeSubscriptions.find((sub) => {
|
|
356
|
+
// If we have a specific subscription to update, match by ID
|
|
357
|
+
if (
|
|
358
|
+
subscriptionToUpdate?.stripeSubscriptionId ||
|
|
359
|
+
ctx.body.subscriptionId
|
|
360
|
+
) {
|
|
361
|
+
return (
|
|
362
|
+
sub.id === subscriptionToUpdate?.stripeSubscriptionId ||
|
|
363
|
+
sub.id === ctx.body.subscriptionId
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
// Only find subscription for the same referenceId to avoid mixing personal and org subscriptions
|
|
367
|
+
if (activeOrTrialingSubscription?.stripeSubscriptionId) {
|
|
368
|
+
return sub.id === activeOrTrialingSubscription.stripeSubscriptionId;
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
});
|
|
372
|
+
|
|
361
373
|
// Also find any incomplete subscription that we can reuse
|
|
362
374
|
const incompleteSubscription = subscriptions.find(
|
|
363
375
|
(sub) => sub.status === "incomplete",
|
|
@@ -375,6 +387,61 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
375
387
|
}
|
|
376
388
|
|
|
377
389
|
if (activeSubscription && customerId) {
|
|
390
|
+
// Find the corresponding database subscription for this Stripe subscription
|
|
391
|
+
let dbSubscription = await ctx.context.adapter.findOne<Subscription>({
|
|
392
|
+
model: "subscription",
|
|
393
|
+
where: [
|
|
394
|
+
{
|
|
395
|
+
field: "stripeSubscriptionId",
|
|
396
|
+
value: activeSubscription.id,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// If no database record exists for this Stripe subscription, update the existing one
|
|
402
|
+
if (!dbSubscription && activeOrTrialingSubscription) {
|
|
403
|
+
await ctx.context.adapter.update<InputSubscription>({
|
|
404
|
+
model: "subscription",
|
|
405
|
+
update: {
|
|
406
|
+
stripeSubscriptionId: activeSubscription.id,
|
|
407
|
+
updatedAt: new Date(),
|
|
408
|
+
},
|
|
409
|
+
where: [
|
|
410
|
+
{
|
|
411
|
+
field: "id",
|
|
412
|
+
value: activeOrTrialingSubscription.id,
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
dbSubscription = activeOrTrialingSubscription;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Resolve price ID if using lookup keys
|
|
420
|
+
let priceIdToUse: string | undefined = undefined;
|
|
421
|
+
if (ctx.body.annual) {
|
|
422
|
+
priceIdToUse = plan.annualDiscountPriceId;
|
|
423
|
+
if (!priceIdToUse && plan.annualDiscountLookupKey) {
|
|
424
|
+
priceIdToUse = await resolvePriceIdFromLookupKey(
|
|
425
|
+
client,
|
|
426
|
+
plan.annualDiscountLookupKey,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
priceIdToUse = plan.priceId;
|
|
431
|
+
if (!priceIdToUse && plan.lookupKey) {
|
|
432
|
+
priceIdToUse = await resolvePriceIdFromLookupKey(
|
|
433
|
+
client,
|
|
434
|
+
plan.lookupKey,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!priceIdToUse) {
|
|
440
|
+
throw ctx.error("BAD_REQUEST", {
|
|
441
|
+
message: "Price ID not found for the selected plan",
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
378
445
|
const { url } = await client.billingPortal.sessions
|
|
379
446
|
.create({
|
|
380
447
|
customer: customerId,
|
|
@@ -393,9 +460,7 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
393
460
|
{
|
|
394
461
|
id: activeSubscription.items.data[0]?.id as string,
|
|
395
462
|
quantity: ctx.body.seats || 1,
|
|
396
|
-
price:
|
|
397
|
-
? plan.annualDiscountPriceId
|
|
398
|
-
: plan.priceId,
|
|
463
|
+
price: priceIdToUse,
|
|
399
464
|
},
|
|
400
465
|
],
|
|
401
466
|
},
|
|
@@ -1235,13 +1300,27 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
1235
1300
|
create: {
|
|
1236
1301
|
async after(user, ctx) {
|
|
1237
1302
|
if (ctx && options.createCustomerOnSignUp) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1303
|
+
let extraCreateParams: Partial<Stripe.CustomerCreateParams> =
|
|
1304
|
+
{};
|
|
1305
|
+
if (options.getCustomerCreateParams) {
|
|
1306
|
+
extraCreateParams = await options.getCustomerCreateParams(
|
|
1307
|
+
user,
|
|
1308
|
+
ctx,
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const params: Stripe.CustomerCreateParams = defu(
|
|
1313
|
+
{
|
|
1314
|
+
email: user.email,
|
|
1315
|
+
name: user.name,
|
|
1316
|
+
metadata: {
|
|
1317
|
+
userId: user.id,
|
|
1318
|
+
},
|
|
1243
1319
|
},
|
|
1244
|
-
|
|
1320
|
+
extraCreateParams,
|
|
1321
|
+
);
|
|
1322
|
+
const stripeCustomer =
|
|
1323
|
+
await client.customers.create(params);
|
|
1245
1324
|
await ctx.context.internalAdapter.updateUser(user.id, {
|
|
1246
1325
|
stripeCustomerId: stripeCustomer.id,
|
|
1247
1326
|
});
|
|
@@ -1258,6 +1337,56 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|
|
1258
1337
|
}
|
|
1259
1338
|
},
|
|
1260
1339
|
},
|
|
1340
|
+
update: {
|
|
1341
|
+
async after(user, ctx) {
|
|
1342
|
+
if (!ctx) return;
|
|
1343
|
+
|
|
1344
|
+
try {
|
|
1345
|
+
// Cast user to include stripeCustomerId (added by the stripe plugin schema)
|
|
1346
|
+
const userWithStripe = user as typeof user & {
|
|
1347
|
+
stripeCustomerId?: string;
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
// Only proceed if user has a Stripe customer ID
|
|
1351
|
+
if (!userWithStripe.stripeCustomerId) return;
|
|
1352
|
+
|
|
1353
|
+
// Get the user from the database to check if email actually changed
|
|
1354
|
+
// The 'user' parameter here is the freshly updated user
|
|
1355
|
+
// We need to check if the Stripe customer's email matches
|
|
1356
|
+
const stripeCustomer = await client.customers.retrieve(
|
|
1357
|
+
userWithStripe.stripeCustomerId,
|
|
1358
|
+
);
|
|
1359
|
+
|
|
1360
|
+
// Check if customer was deleted
|
|
1361
|
+
if (stripeCustomer.deleted) {
|
|
1362
|
+
ctx.context.logger.warn(
|
|
1363
|
+
`Stripe customer ${userWithStripe.stripeCustomerId} was deleted, cannot update email`,
|
|
1364
|
+
);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// If Stripe customer email doesn't match the user's current email, update it
|
|
1369
|
+
if (stripeCustomer.email !== user.email) {
|
|
1370
|
+
await client.customers.update(
|
|
1371
|
+
userWithStripe.stripeCustomerId,
|
|
1372
|
+
{
|
|
1373
|
+
email: user.email,
|
|
1374
|
+
},
|
|
1375
|
+
);
|
|
1376
|
+
ctx.context.logger.info(
|
|
1377
|
+
`Updated Stripe customer email from ${stripeCustomer.email} to ${user.email}`,
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
} catch (e: any) {
|
|
1381
|
+
// Ignore errors - this is a best-effort sync
|
|
1382
|
+
// Email might have been deleted or Stripe customer might not exist
|
|
1383
|
+
ctx.context.logger.error(
|
|
1384
|
+
`Failed to sync email to Stripe customer: ${e.message}`,
|
|
1385
|
+
e,
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
},
|
|
1389
|
+
},
|
|
1261
1390
|
},
|
|
1262
1391
|
},
|
|
1263
1392
|
},
|
package/src/stripe.test.ts
CHANGED
|
@@ -17,6 +17,16 @@ describe("stripe", async () => {
|
|
|
17
17
|
},
|
|
18
18
|
customers: {
|
|
19
19
|
create: vi.fn().mockResolvedValue({ id: "cus_mock123" }),
|
|
20
|
+
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
21
|
+
retrieve: vi.fn().mockResolvedValue({
|
|
22
|
+
id: "cus_mock123",
|
|
23
|
+
email: "test@email.com",
|
|
24
|
+
deleted: false,
|
|
25
|
+
}),
|
|
26
|
+
update: vi.fn().mockResolvedValue({
|
|
27
|
+
id: "cus_mock123",
|
|
28
|
+
email: "newemail@example.com",
|
|
29
|
+
}),
|
|
20
30
|
},
|
|
21
31
|
checkout: {
|
|
22
32
|
sessions: {
|
|
@@ -1188,6 +1198,141 @@ describe("stripe", async () => {
|
|
|
1188
1198
|
expect(hasTrialData).toBe(true);
|
|
1189
1199
|
});
|
|
1190
1200
|
|
|
1201
|
+
it("should upgrade existing subscription instead of creating new one", async () => {
|
|
1202
|
+
// Reset mocks for this test
|
|
1203
|
+
vi.clearAllMocks();
|
|
1204
|
+
|
|
1205
|
+
// Create a user
|
|
1206
|
+
const userRes = await authClient.signUp.email(
|
|
1207
|
+
{ ...testUser, email: "upgrade-existing@email.com" },
|
|
1208
|
+
{ throw: true },
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
const headers = new Headers();
|
|
1212
|
+
await authClient.signIn.email(
|
|
1213
|
+
{ ...testUser, email: "upgrade-existing@email.com" },
|
|
1214
|
+
{
|
|
1215
|
+
throw: true,
|
|
1216
|
+
onSuccess: setCookieToHeader(headers),
|
|
1217
|
+
},
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1220
|
+
// Mock customers.list to find existing customer
|
|
1221
|
+
mockStripe.customers.list.mockResolvedValueOnce({
|
|
1222
|
+
data: [{ id: "cus_test_123" }],
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// First create a starter subscription
|
|
1226
|
+
await authClient.subscription.upgrade({
|
|
1227
|
+
plan: "starter",
|
|
1228
|
+
fetchOptions: { headers },
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
// Simulate the subscription being active
|
|
1232
|
+
const starterSub = await ctx.adapter.findOne<Subscription>({
|
|
1233
|
+
model: "subscription",
|
|
1234
|
+
where: [
|
|
1235
|
+
{
|
|
1236
|
+
field: "referenceId",
|
|
1237
|
+
value: userRes.user.id,
|
|
1238
|
+
},
|
|
1239
|
+
],
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
await ctx.adapter.update({
|
|
1243
|
+
model: "subscription",
|
|
1244
|
+
update: {
|
|
1245
|
+
status: "active",
|
|
1246
|
+
stripeSubscriptionId: "sub_active_test_123",
|
|
1247
|
+
stripeCustomerId: "cus_mock123", // Use the same customer ID as the mock
|
|
1248
|
+
},
|
|
1249
|
+
where: [
|
|
1250
|
+
{
|
|
1251
|
+
field: "id",
|
|
1252
|
+
value: starterSub!.id,
|
|
1253
|
+
},
|
|
1254
|
+
],
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
// Also update the user with the Stripe customer ID
|
|
1258
|
+
await ctx.adapter.update({
|
|
1259
|
+
model: "user",
|
|
1260
|
+
update: {
|
|
1261
|
+
stripeCustomerId: "cus_mock123",
|
|
1262
|
+
},
|
|
1263
|
+
where: [
|
|
1264
|
+
{
|
|
1265
|
+
field: "id",
|
|
1266
|
+
value: userRes.user.id,
|
|
1267
|
+
},
|
|
1268
|
+
],
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
// Mock Stripe subscriptions.list to return the active subscription
|
|
1272
|
+
mockStripe.subscriptions.list.mockResolvedValueOnce({
|
|
1273
|
+
data: [
|
|
1274
|
+
{
|
|
1275
|
+
id: "sub_active_test_123",
|
|
1276
|
+
status: "active",
|
|
1277
|
+
items: {
|
|
1278
|
+
data: [
|
|
1279
|
+
{
|
|
1280
|
+
id: "si_test_123",
|
|
1281
|
+
price: { id: process.env.STRIPE_PRICE_ID_1 },
|
|
1282
|
+
quantity: 1,
|
|
1283
|
+
current_period_start: Math.floor(Date.now() / 1000),
|
|
1284
|
+
current_period_end:
|
|
1285
|
+
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1286
|
+
},
|
|
1287
|
+
],
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
],
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
// Clear mock calls before the upgrade
|
|
1294
|
+
mockStripe.checkout.sessions.create.mockClear();
|
|
1295
|
+
mockStripe.billingPortal.sessions.create.mockClear();
|
|
1296
|
+
|
|
1297
|
+
// Now upgrade to premium plan - should use billing portal to update existing subscription
|
|
1298
|
+
const upgradeRes = await authClient.subscription.upgrade({
|
|
1299
|
+
plan: "premium",
|
|
1300
|
+
fetchOptions: { headers },
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
// Verify that billing portal was called (indicating update, not new subscription)
|
|
1304
|
+
expect(mockStripe.billingPortal.sessions.create).toHaveBeenCalledWith(
|
|
1305
|
+
expect.objectContaining({
|
|
1306
|
+
customer: "cus_mock123",
|
|
1307
|
+
flow_data: expect.objectContaining({
|
|
1308
|
+
type: "subscription_update_confirm",
|
|
1309
|
+
subscription_update_confirm: expect.objectContaining({
|
|
1310
|
+
subscription: "sub_active_test_123",
|
|
1311
|
+
}),
|
|
1312
|
+
}),
|
|
1313
|
+
}),
|
|
1314
|
+
);
|
|
1315
|
+
|
|
1316
|
+
// Should not create a new checkout session
|
|
1317
|
+
expect(mockStripe.checkout.sessions.create).not.toHaveBeenCalled();
|
|
1318
|
+
|
|
1319
|
+
// Verify the response has a redirect URL
|
|
1320
|
+
expect(upgradeRes.data?.url).toBe("https://billing.stripe.com/mock");
|
|
1321
|
+
expect(upgradeRes.data?.redirect).toBe(true);
|
|
1322
|
+
|
|
1323
|
+
// Verify no new subscription was created in the database
|
|
1324
|
+
const allSubs = await ctx.adapter.findMany<Subscription>({
|
|
1325
|
+
model: "subscription",
|
|
1326
|
+
where: [
|
|
1327
|
+
{
|
|
1328
|
+
field: "referenceId",
|
|
1329
|
+
value: userRes.user.id,
|
|
1330
|
+
},
|
|
1331
|
+
],
|
|
1332
|
+
});
|
|
1333
|
+
expect(allSubs).toHaveLength(1); // Should still have only one subscription
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1191
1336
|
it("should prevent multiple free trials across different plans", async () => {
|
|
1192
1337
|
// Create a user
|
|
1193
1338
|
const userRes = await authClient.signUp.email(
|
|
@@ -1280,4 +1425,318 @@ describe("stripe", async () => {
|
|
|
1280
1425
|
});
|
|
1281
1426
|
expect(hasEverTrialed).toBe(true);
|
|
1282
1427
|
});
|
|
1428
|
+
|
|
1429
|
+
it("should update stripe customer email when user email changes", async () => {
|
|
1430
|
+
// Setup mock for customer retrieve and update
|
|
1431
|
+
mockStripe.customers.retrieve = vi.fn().mockResolvedValue({
|
|
1432
|
+
id: "cus_mock123",
|
|
1433
|
+
email: "test@email.com",
|
|
1434
|
+
deleted: false,
|
|
1435
|
+
});
|
|
1436
|
+
mockStripe.customers.update = vi.fn().mockResolvedValue({
|
|
1437
|
+
id: "cus_mock123",
|
|
1438
|
+
email: "newemail@example.com",
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
// Sign up a user
|
|
1442
|
+
const userRes = await authClient.signUp.email(testUser, {
|
|
1443
|
+
throw: true,
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
expect(userRes.user).toBeDefined();
|
|
1447
|
+
|
|
1448
|
+
// Verify customer was created during signup
|
|
1449
|
+
expect(mockStripe.customers.create).toHaveBeenCalledWith({
|
|
1450
|
+
email: testUser.email,
|
|
1451
|
+
name: testUser.name,
|
|
1452
|
+
metadata: {
|
|
1453
|
+
userId: userRes.user.id,
|
|
1454
|
+
},
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
// Clear mocks to track the update
|
|
1458
|
+
vi.clearAllMocks();
|
|
1459
|
+
|
|
1460
|
+
// Re-setup the retrieve mock for the update flow
|
|
1461
|
+
mockStripe.customers.retrieve = vi.fn().mockResolvedValue({
|
|
1462
|
+
id: "cus_mock123",
|
|
1463
|
+
email: "test@email.com",
|
|
1464
|
+
deleted: false,
|
|
1465
|
+
});
|
|
1466
|
+
mockStripe.customers.update = vi.fn().mockResolvedValue({
|
|
1467
|
+
id: "cus_mock123",
|
|
1468
|
+
email: "newemail@example.com",
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
// Create a mock request context
|
|
1472
|
+
const mockRequest = new Request("http://localhost:3000/api/test", {
|
|
1473
|
+
method: "POST",
|
|
1474
|
+
headers: {
|
|
1475
|
+
"content-type": "application/json",
|
|
1476
|
+
},
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
// Update the user's email using internal adapter (which triggers hooks)
|
|
1480
|
+
await ctx.internalAdapter.updateUserByEmail(
|
|
1481
|
+
testUser.email,
|
|
1482
|
+
{
|
|
1483
|
+
email: "newemail@example.com",
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
request: mockRequest,
|
|
1487
|
+
context: ctx,
|
|
1488
|
+
} as any,
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
// Verify that Stripe customer.retrieve was called
|
|
1492
|
+
expect(mockStripe.customers.retrieve).toHaveBeenCalledWith("cus_mock123");
|
|
1493
|
+
|
|
1494
|
+
// Verify that Stripe customer.update was called with the new email
|
|
1495
|
+
expect(mockStripe.customers.update).toHaveBeenCalledWith("cus_mock123", {
|
|
1496
|
+
email: "newemail@example.com",
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
describe("getCustomerCreateParams", () => {
|
|
1501
|
+
it("should call getCustomerCreateParams and merge with default params", async () => {
|
|
1502
|
+
const getCustomerCreateParamsMock = vi
|
|
1503
|
+
.fn()
|
|
1504
|
+
.mockResolvedValue({ metadata: { customField: "customValue" } });
|
|
1505
|
+
|
|
1506
|
+
const testOptions = {
|
|
1507
|
+
...stripeOptions,
|
|
1508
|
+
createCustomerOnSignUp: true,
|
|
1509
|
+
getCustomerCreateParams: getCustomerCreateParamsMock,
|
|
1510
|
+
} satisfies StripeOptions;
|
|
1511
|
+
|
|
1512
|
+
const testAuth = betterAuth({
|
|
1513
|
+
database: memory,
|
|
1514
|
+
baseURL: "http://localhost:3000",
|
|
1515
|
+
emailAndPassword: {
|
|
1516
|
+
enabled: true,
|
|
1517
|
+
},
|
|
1518
|
+
plugins: [stripe(testOptions)],
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
const testAuthClient = createAuthClient({
|
|
1522
|
+
baseURL: "http://localhost:3000",
|
|
1523
|
+
plugins: [bearer(), stripeClient({ subscription: true })],
|
|
1524
|
+
fetchOptions: {
|
|
1525
|
+
customFetchImpl: async (url, init) =>
|
|
1526
|
+
testAuth.handler(new Request(url, init)),
|
|
1527
|
+
},
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
// Sign up a user
|
|
1531
|
+
const userRes = await testAuthClient.signUp.email(
|
|
1532
|
+
{
|
|
1533
|
+
email: "custom-params@email.com",
|
|
1534
|
+
password: "password",
|
|
1535
|
+
name: "Custom User",
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
throw: true,
|
|
1539
|
+
},
|
|
1540
|
+
);
|
|
1541
|
+
|
|
1542
|
+
// Verify getCustomerCreateParams was called
|
|
1543
|
+
expect(getCustomerCreateParamsMock).toHaveBeenCalledWith(
|
|
1544
|
+
expect.objectContaining({
|
|
1545
|
+
id: userRes.user.id,
|
|
1546
|
+
email: "custom-params@email.com",
|
|
1547
|
+
name: "Custom User",
|
|
1548
|
+
}),
|
|
1549
|
+
expect.objectContaining({
|
|
1550
|
+
context: expect.any(Object),
|
|
1551
|
+
}),
|
|
1552
|
+
);
|
|
1553
|
+
|
|
1554
|
+
// Verify customer was created with merged params
|
|
1555
|
+
expect(mockStripe.customers.create).toHaveBeenCalledWith(
|
|
1556
|
+
expect.objectContaining({
|
|
1557
|
+
email: "custom-params@email.com",
|
|
1558
|
+
name: "Custom User",
|
|
1559
|
+
metadata: expect.objectContaining({
|
|
1560
|
+
userId: userRes.user.id,
|
|
1561
|
+
customField: "customValue",
|
|
1562
|
+
}),
|
|
1563
|
+
}),
|
|
1564
|
+
);
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1567
|
+
it("should use getCustomerCreateParams to add custom address", async () => {
|
|
1568
|
+
const getCustomerCreateParamsMock = vi.fn().mockResolvedValue({
|
|
1569
|
+
address: {
|
|
1570
|
+
line1: "123 Main St",
|
|
1571
|
+
city: "San Francisco",
|
|
1572
|
+
state: "CA",
|
|
1573
|
+
postal_code: "94111",
|
|
1574
|
+
country: "US",
|
|
1575
|
+
},
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
const testOptions = {
|
|
1579
|
+
...stripeOptions,
|
|
1580
|
+
createCustomerOnSignUp: true,
|
|
1581
|
+
getCustomerCreateParams: getCustomerCreateParamsMock,
|
|
1582
|
+
} satisfies StripeOptions;
|
|
1583
|
+
|
|
1584
|
+
const testAuth = betterAuth({
|
|
1585
|
+
database: memory,
|
|
1586
|
+
baseURL: "http://localhost:3000",
|
|
1587
|
+
emailAndPassword: {
|
|
1588
|
+
enabled: true,
|
|
1589
|
+
},
|
|
1590
|
+
plugins: [stripe(testOptions)],
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
const testAuthClient = createAuthClient({
|
|
1594
|
+
baseURL: "http://localhost:3000",
|
|
1595
|
+
plugins: [bearer(), stripeClient({ subscription: true })],
|
|
1596
|
+
fetchOptions: {
|
|
1597
|
+
customFetchImpl: async (url, init) =>
|
|
1598
|
+
testAuth.handler(new Request(url, init)),
|
|
1599
|
+
},
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
// Sign up a user
|
|
1603
|
+
await testAuthClient.signUp.email(
|
|
1604
|
+
{
|
|
1605
|
+
email: "address-user@email.com",
|
|
1606
|
+
password: "password",
|
|
1607
|
+
name: "Address User",
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
throw: true,
|
|
1611
|
+
},
|
|
1612
|
+
);
|
|
1613
|
+
|
|
1614
|
+
// Verify customer was created with address
|
|
1615
|
+
expect(mockStripe.customers.create).toHaveBeenCalledWith(
|
|
1616
|
+
expect.objectContaining({
|
|
1617
|
+
email: "address-user@email.com",
|
|
1618
|
+
name: "Address User",
|
|
1619
|
+
address: {
|
|
1620
|
+
line1: "123 Main St",
|
|
1621
|
+
city: "San Francisco",
|
|
1622
|
+
state: "CA",
|
|
1623
|
+
postal_code: "94111",
|
|
1624
|
+
country: "US",
|
|
1625
|
+
},
|
|
1626
|
+
metadata: expect.objectContaining({
|
|
1627
|
+
userId: expect.any(String),
|
|
1628
|
+
}),
|
|
1629
|
+
}),
|
|
1630
|
+
);
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it("should properly merge nested objects using defu", async () => {
|
|
1634
|
+
const getCustomerCreateParamsMock = vi.fn().mockResolvedValue({
|
|
1635
|
+
metadata: {
|
|
1636
|
+
customField: "customValue",
|
|
1637
|
+
anotherField: "anotherValue",
|
|
1638
|
+
},
|
|
1639
|
+
phone: "+1234567890",
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
const testOptions = {
|
|
1643
|
+
...stripeOptions,
|
|
1644
|
+
createCustomerOnSignUp: true,
|
|
1645
|
+
getCustomerCreateParams: getCustomerCreateParamsMock,
|
|
1646
|
+
} satisfies StripeOptions;
|
|
1647
|
+
|
|
1648
|
+
const testAuth = betterAuth({
|
|
1649
|
+
database: memory,
|
|
1650
|
+
baseURL: "http://localhost:3000",
|
|
1651
|
+
emailAndPassword: {
|
|
1652
|
+
enabled: true,
|
|
1653
|
+
},
|
|
1654
|
+
plugins: [stripe(testOptions)],
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
const testAuthClient = createAuthClient({
|
|
1658
|
+
baseURL: "http://localhost:3000",
|
|
1659
|
+
plugins: [bearer(), stripeClient({ subscription: true })],
|
|
1660
|
+
fetchOptions: {
|
|
1661
|
+
customFetchImpl: async (url, init) =>
|
|
1662
|
+
testAuth.handler(new Request(url, init)),
|
|
1663
|
+
},
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
// Sign up a user
|
|
1667
|
+
const userRes = await testAuthClient.signUp.email(
|
|
1668
|
+
{
|
|
1669
|
+
email: "merge-test@email.com",
|
|
1670
|
+
password: "password",
|
|
1671
|
+
name: "Merge User",
|
|
1672
|
+
},
|
|
1673
|
+
{
|
|
1674
|
+
throw: true,
|
|
1675
|
+
},
|
|
1676
|
+
);
|
|
1677
|
+
|
|
1678
|
+
// Verify customer was created with properly merged params
|
|
1679
|
+
// defu merges objects and preserves all fields
|
|
1680
|
+
expect(mockStripe.customers.create).toHaveBeenCalledWith(
|
|
1681
|
+
expect.objectContaining({
|
|
1682
|
+
email: "merge-test@email.com",
|
|
1683
|
+
name: "Merge User",
|
|
1684
|
+
phone: "+1234567890",
|
|
1685
|
+
metadata: {
|
|
1686
|
+
userId: userRes.user.id,
|
|
1687
|
+
customField: "customValue",
|
|
1688
|
+
anotherField: "anotherValue",
|
|
1689
|
+
},
|
|
1690
|
+
}),
|
|
1691
|
+
);
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
it("should work without getCustomerCreateParams", async () => {
|
|
1695
|
+
// This test ensures backward compatibility
|
|
1696
|
+
const testOptions = {
|
|
1697
|
+
...stripeOptions,
|
|
1698
|
+
createCustomerOnSignUp: true,
|
|
1699
|
+
// No getCustomerCreateParams provided
|
|
1700
|
+
} satisfies StripeOptions;
|
|
1701
|
+
|
|
1702
|
+
const testAuth = betterAuth({
|
|
1703
|
+
database: memory,
|
|
1704
|
+
baseURL: "http://localhost:3000",
|
|
1705
|
+
emailAndPassword: {
|
|
1706
|
+
enabled: true,
|
|
1707
|
+
},
|
|
1708
|
+
plugins: [stripe(testOptions)],
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
const testAuthClient = createAuthClient({
|
|
1712
|
+
baseURL: "http://localhost:3000",
|
|
1713
|
+
plugins: [bearer(), stripeClient({ subscription: true })],
|
|
1714
|
+
fetchOptions: {
|
|
1715
|
+
customFetchImpl: async (url, init) =>
|
|
1716
|
+
testAuth.handler(new Request(url, init)),
|
|
1717
|
+
},
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
// Sign up a user
|
|
1721
|
+
const userRes = await testAuthClient.signUp.email(
|
|
1722
|
+
{
|
|
1723
|
+
email: "no-custom-params@email.com",
|
|
1724
|
+
password: "password",
|
|
1725
|
+
name: "Default User",
|
|
1726
|
+
},
|
|
1727
|
+
{
|
|
1728
|
+
throw: true,
|
|
1729
|
+
},
|
|
1730
|
+
);
|
|
1731
|
+
|
|
1732
|
+
// Verify customer was created with default params only
|
|
1733
|
+
expect(mockStripe.customers.create).toHaveBeenCalledWith({
|
|
1734
|
+
email: "no-custom-params@email.com",
|
|
1735
|
+
name: "Default User",
|
|
1736
|
+
metadata: {
|
|
1737
|
+
userId: userRes.user.id,
|
|
1738
|
+
},
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1741
|
+
});
|
|
1283
1742
|
});
|
package/src/types.ts
CHANGED
|
@@ -199,12 +199,9 @@ export interface StripeOptions {
|
|
|
199
199
|
* @returns
|
|
200
200
|
*/
|
|
201
201
|
getCustomerCreateParams?: (
|
|
202
|
-
|
|
203
|
-
user: User;
|
|
204
|
-
session: Session;
|
|
205
|
-
},
|
|
202
|
+
user: User,
|
|
206
203
|
ctx: GenericEndpointContext,
|
|
207
|
-
) => Promise<
|
|
204
|
+
) => Promise<Partial<Stripe.CustomerCreateParams>>;
|
|
208
205
|
/**
|
|
209
206
|
* Subscriptions
|
|
210
207
|
*/
|