mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-23 23:52:05 -05:00
feat(stripe): create billing portal session (#3625)
* feat(stripe) : add billing portal session endpoint * create billing portal session * update docs * chore: changeset * chore: fix stripe * chore: remove comment * chore: update docs --------- Co-authored-by: Bereket Engida <Bekacru@gmail.com> Co-authored-by: Bereket Engida <86073083+Bekacru@users.noreply.github.com>
This commit is contained in:
5
.changeset/kind-rice-poke.md
Normal file
5
.changeset/kind-rice-poke.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@better-auth/stripe": patch
|
||||
---
|
||||
|
||||
A new API endpoint to create Stripe billing portal sessions, allowing users to manage subscriptions, payment methods, and billing history
|
||||
@@ -384,6 +384,31 @@ This will reactivate a subscription that was previously set to cancel at the end
|
||||
|
||||
> **Note:** This only works for subscriptions that are still active but marked to cancel at the end of the period. It cannot restore subscriptions that have already ended.
|
||||
|
||||
#### Creating Billing Portal Sessions
|
||||
|
||||
To create a [Stripe billing portal session](https://docs.stripe.com/api/customer_portal/sessions/create) where customers can manage their subscriptions, update payment methods, and view billing history:
|
||||
|
||||
<APIMethod
|
||||
path="/subscription/billing-portal"
|
||||
method="POST"
|
||||
requireSession
|
||||
>
|
||||
```ts
|
||||
type createBillingPortal = {
|
||||
/**
|
||||
* Reference id of the subscription to upgrade.
|
||||
*/
|
||||
referenceId?: string = "123"
|
||||
/**
|
||||
* Return URL to redirect back after successful subscription.
|
||||
*/
|
||||
returnUrl?: string
|
||||
}
|
||||
```
|
||||
</APIMethod>
|
||||
|
||||
This endpoint creates a Stripe billing portal session and returns a URL in the response as `data.url`. You can redirect users to this URL to allow them to manage their subscription, payment methods, and billing history.
|
||||
|
||||
### Reference System
|
||||
|
||||
By default, subscriptions are associated with the user ID. However, you can use a custom reference ID to associate subscriptions with other entities, such as organizations:
|
||||
@@ -822,4 +847,4 @@ For local development, you can use the Stripe CLI to forward webhooks to your lo
|
||||
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
|
||||
```
|
||||
|
||||
This will provide you with a webhook signing secret that you can use in your local environment.
|
||||
This will provide you with a webhook signing secret that you can use in your local environment.
|
||||
@@ -69,7 +69,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
| "upgrade-subscription"
|
||||
| "list-subscription"
|
||||
| "cancel-subscription"
|
||||
| "restore-subscription",
|
||||
| "restore-subscription"
|
||||
| "billing-portal",
|
||||
) =>
|
||||
createAuthMiddleware(async (ctx) => {
|
||||
const session = ctx.context.session;
|
||||
@@ -1033,6 +1034,73 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
||||
throw ctx.redirect(getUrl(ctx, callbackURL));
|
||||
},
|
||||
),
|
||||
createBillingPortal: createAuthEndpoint(
|
||||
"/subscription/billing-portal",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
referenceId: z.string().optional(),
|
||||
returnUrl: z.string().default("/"),
|
||||
}),
|
||||
use: [
|
||||
sessionMiddleware,
|
||||
originCheck((ctx) => ctx.body.returnUrl),
|
||||
referenceMiddleware("billing-portal"),
|
||||
],
|
||||
},
|
||||
async (ctx) => {
|
||||
const { user } = ctx.context.session;
|
||||
const referenceId = ctx.body.referenceId || user.id;
|
||||
|
||||
let customerId = user.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
const subscription = await ctx.context.adapter
|
||||
.findMany<Subscription>({
|
||||
model: "subscription",
|
||||
where: [
|
||||
{
|
||||
field: "referenceId",
|
||||
value: referenceId,
|
||||
},
|
||||
],
|
||||
})
|
||||
.then((subs) =>
|
||||
subs.find(
|
||||
(sub) => sub.status === "active" || sub.status === "trialing",
|
||||
),
|
||||
);
|
||||
|
||||
customerId = subscription?.stripeCustomerId;
|
||||
}
|
||||
|
||||
if (!customerId) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "No Stripe customer found for this user",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { url } = await client.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url: getUrl(ctx, ctx.body.returnUrl),
|
||||
});
|
||||
|
||||
return ctx.json({
|
||||
url,
|
||||
redirect: true,
|
||||
});
|
||||
} catch (error: any) {
|
||||
ctx.context.logger.error(
|
||||
"Error creating billing portal session",
|
||||
error,
|
||||
);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
} as const;
|
||||
return {
|
||||
id: "stripe",
|
||||
|
||||
@@ -844,4 +844,40 @@ describe("stripe", async () => {
|
||||
|
||||
expect(mockStripe.customers.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should create billing portal session", async () => {
|
||||
await authClient.signUp.email(
|
||||
{
|
||||
...testUser,
|
||||
email: "billing-portal@email.com",
|
||||
},
|
||||
{
|
||||
throw: true,
|
||||
},
|
||||
);
|
||||
|
||||
const headers = new Headers();
|
||||
await authClient.signIn.email(
|
||||
{
|
||||
...testUser,
|
||||
email: "billing-portal@email.com",
|
||||
},
|
||||
{
|
||||
throw: true,
|
||||
onSuccess: setCookieToHeader(headers),
|
||||
},
|
||||
);
|
||||
const billingPortalRes = await authClient.subscription.billingPortal({
|
||||
returnUrl: "/dashboard",
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
expect(billingPortalRes.data?.url).toBe("https://billing.stripe.com/mock");
|
||||
expect(billingPortalRes.data?.redirect).toBe(true);
|
||||
expect(mockStripe.billingPortal.sessions.create).toHaveBeenCalledWith({
|
||||
customer: expect.any(String),
|
||||
return_url: "http://localhost:3000/dashboard",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -274,7 +274,8 @@ export interface StripeOptions {
|
||||
| "upgrade-subscription"
|
||||
| "list-subscription"
|
||||
| "cancel-subscription"
|
||||
| "restore-subscription";
|
||||
| "restore-subscription"
|
||||
| "billing-portal";
|
||||
},
|
||||
ctx: GenericEndpointContext,
|
||||
) => Promise<boolean>;
|
||||
|
||||
Reference in New Issue
Block a user