kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add stripe billing portal
rodzic
efe4b122d3
commit
e802db55eb
|
@ -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 })
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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?: {
|
||||
|
|
Ładowanie…
Reference in New Issue