feat: add stripe billing portal

pull/715/head
Travis Fischer 2025-06-20 13:29:24 -05:00
rodzic efe4b122d3
commit e802db55eb
5 zmienionych plików z 138 dodań i 8 usunięć

Wyświetl plik

@ -0,0 +1,54 @@
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
import type { AuthenticatedHonoEnv } from '@/lib/types'
import { upsertStripeCustomer } from '@/lib/billing/upsert-stripe-customer'
import { env } from '@/lib/env'
import { stripe } from '@/lib/external/stripe'
import {
openapiAuthenticatedSecuritySchemas,
openapiErrorResponse404,
openapiErrorResponse409,
openapiErrorResponse410,
openapiErrorResponses
} from '@/lib/openapi-utils'
const route = createRoute({
description:
'Creates a Stripe billing portal session for the authenticated user.',
tags: ['consumers'],
operationId: 'createBillingPortalSession',
method: 'post',
path: 'consumers/billing-portal',
security: openapiAuthenticatedSecuritySchemas,
responses: {
200: {
description: 'A billing portal session URL',
content: {
'application/json': {
schema: z.object({
url: z.string().url()
})
}
}
},
...openapiErrorResponses,
...openapiErrorResponse404,
...openapiErrorResponse409,
...openapiErrorResponse410
}
})
export function registerV1CreateBillingPortalSession(
app: OpenAPIHono<AuthenticatedHonoEnv>
) {
return app.openapi(route, async (c) => {
const { stripeCustomer } = await upsertStripeCustomer(c)
const portalSession = await stripe.billingPortal.sessions.create({
customer: stripeCustomer.id,
return_url: `${env.AGENTIC_WEB_BASE_URL}/app/consumers`
})
return c.json({ url: portalSession.url })
})
}

Wyświetl plik

@ -13,6 +13,7 @@ import { registerV1SignInWithPassword } from './auth/sign-in-with-password'
import { registerV1SignUpWithPassword } from './auth/sign-up-with-password'
import { registerV1AdminActivateConsumer } from './consumers/admin-activate-consumer'
import { registerV1AdminGetConsumerByToken } from './consumers/admin-get-consumer-by-token'
import { registerV1CreateBillingPortalSession } from './consumers/create-billing-portal-session'
import { registerV1CreateConsumer } from './consumers/create-consumer'
import { registerV1CreateConsumerBillingPortalSession } from './consumers/create-consumer-billing-portal-session'
import { registerV1CreateConsumerCheckoutSession } from './consumers/create-consumer-checkout-session'
@ -121,6 +122,7 @@ registerV1UpdateProject(privateRouter)
// Consumers
registerV1GetConsumerByProjectIdentifier(privateRouter) // must be before `registerV1GetConsumer`
registerV1CreateBillingPortalSession(privateRouter)
registerV1GetConsumer(privateRouter)
registerV1CreateConsumer(privateRouter)
registerV1CreateConsumerCheckoutSession(privateRouter)

Wyświetl plik

@ -1,18 +1,31 @@
'use client'
import { useCallback } from 'react'
import { useAuthenticatedAgentic } from '@/components/agentic-provider'
import { AppConsumersList } from '@/components/app-consumers-list'
import { Button } from '@/components/ui/button'
export function AppConsumersIndex() {
const ctx = useAuthenticatedAgentic()
const onManageSubscriptions = useCallback(async () => {
const { url } = await ctx!.api.createBillingPortalSession()
globalThis.open(url, '_blank')
}, [ctx])
return (
<>
<section>
<h1
className='text-center text-balance leading-snug md:leading-none
<h1
className='text-center text-balance leading-snug md:leading-none
text-4xl font-extrabold'
>
Your Subscriptions
</h1>
>
Subscriptions
</h1>
<AppConsumersList />
</section>
<Button onClick={onManageSubscriptions}>Manage your subscriptions</Button>
<AppConsumersList />
</>
)
}

Wyświetl plik

@ -561,6 +561,21 @@ export class AgenticApiClient {
.json()
}
/**
* Creates a Stripe Billing Portal Session for the authenticated user.
*/
async createBillingPortalSession(
searchParams: OperationParameters<'createBillingPortalSession'> = {}
): Promise<{
url: string
}> {
return this.ky
.post(`v1/consumers/billing-portal`, {
searchParams: sanitizeSearchParams(searchParams)
})
.json()
}
/**
* Creates a Stripe Billing Portal Session for a customer.
*/

Wyświetl plik

@ -300,6 +300,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/consumers/billing-portal": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** @description Creates a Stripe billing portal session for the authenticated user. */
post: operations["createBillingPortalSession"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/consumers/{consumerId}": {
parameters: {
query?: never;
@ -1779,6 +1796,35 @@ export interface operations {
404: components["responses"]["404"];
};
};
createBillingPortalSession: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description A billing portal session URL */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** Format: uri */
url: string;
};
};
};
400: components["responses"]["400"];
401: components["responses"]["401"];
403: components["responses"]["403"];
404: components["responses"]["404"];
409: components["responses"]["409"];
410: components["responses"]["410"];
};
};
getConsumer: {
parameters: {
query?: {