kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: more work on public project pricing plans
rodzic
15a5fd94b3
commit
32e1d92a2a
|
@ -59,6 +59,7 @@
|
||||||
"next-themes": "catalog:",
|
"next-themes": "catalog:",
|
||||||
"plur": "catalog:",
|
"plur": "catalog:",
|
||||||
"posthog-js": "catalog:",
|
"posthog-js": "catalog:",
|
||||||
|
"pretty-ms": "^9.2.0",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
"react-infinite-scroll-hook": "catalog:",
|
"react-infinite-scroll-hook": "catalog:",
|
||||||
|
|
|
@ -575,7 +575,7 @@ function ProjectHeader({
|
||||||
<HeroButton
|
<HeroButton
|
||||||
heroVariant='orange'
|
heroVariant='orange'
|
||||||
className='justify-self-end'
|
className='justify-self-end'
|
||||||
disabled={tab === 'pricing'}
|
disabled={tab === 'pricing' && !!ctx?.isAuthenticated}
|
||||||
asChild={tab !== 'pricing'}
|
asChild={tab !== 'pricing'}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -47,7 +47,7 @@ export function ProjectPricingPlans({
|
||||||
const currentPricingIntervalPlans =
|
const currentPricingIntervalPlans =
|
||||||
pricingPlansByInterval[pricingInterval] ?? []
|
pricingPlansByInterval[pricingInterval] ?? []
|
||||||
|
|
||||||
// TODO: add support for different pricing intervals
|
// TODO: add support for different pricing intervals and switching between them
|
||||||
const numPricingPlans = currentPricingIntervalPlans.length || 1
|
const numPricingPlans = currentPricingIntervalPlans.length || 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,12 +5,21 @@ import type {
|
||||||
Project
|
Project
|
||||||
} from '@agentic/platform-types'
|
} from '@agentic/platform-types'
|
||||||
import humanNumber from 'human-number'
|
import humanNumber from 'human-number'
|
||||||
import { Loader2Icon, PlusIcon } from 'lucide-react'
|
import {
|
||||||
|
CornerDownRightIcon,
|
||||||
|
Loader2Icon,
|
||||||
|
PlusIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
ShieldMinusIcon
|
||||||
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import plur from 'plur'
|
import plur from 'plur'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { pricingAmountToFixedString } from '@/lib/utils'
|
import {
|
||||||
|
getRateLimitIntervalLabel,
|
||||||
|
pricingAmountToFixedString
|
||||||
|
} from '@/lib/utils'
|
||||||
|
|
||||||
// const intervalToLabelMap: Record<PricingInterval, string> = {
|
// const intervalToLabelMap: Record<PricingInterval, string> = {
|
||||||
// day: 'daily',
|
// day: 'daily',
|
||||||
|
@ -41,10 +50,10 @@ export function ProjectPricingPlan({
|
||||||
const requestsLineItem = lineItems.find(
|
const requestsLineItem = lineItems.find(
|
||||||
(lineItem) => lineItem.slug === 'requests'
|
(lineItem) => lineItem.slug === 'requests'
|
||||||
)
|
)
|
||||||
|
const isFreePlan = plan.slug === 'free'
|
||||||
|
|
||||||
// TODO: rate-limits
|
const deployment = project.lastPublishedDeployment
|
||||||
// const deployment = project.lastPublishedDeployment
|
const requestsRateLimit = plan.rateLimit ?? deployment?.defaultRateLimit
|
||||||
// const requestsRateLimit = plan.rateLimit ?? deployment?.defaultRateLimit
|
|
||||||
|
|
||||||
// TODO: support custom line-items
|
// TODO: support custom line-items
|
||||||
// const customLineItems = lineItems.find(
|
// const customLineItems = lineItems.find(
|
||||||
|
@ -53,8 +62,7 @@ export function ProjectPricingPlan({
|
||||||
|
|
||||||
// TODO: support defaultAggregation
|
// TODO: support defaultAggregation
|
||||||
// TODO: support trialPeriodDays
|
// TODO: support trialPeriodDays
|
||||||
|
// TODO: highlight if any tools are disabled on this pricing plan
|
||||||
// TODO: add rate-limits and finesse free tier to not be so bare-bones
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='justify-self-center w-full grid grid-cols-1 rounded-[2rem] shadow-[inset_0_0_2px_1px_#ffffff4d] ring-1 ring-black/5 max-lg:mx-auto max-lg:w-full max-lg:max-w-md max-w-lg'>
|
<div className='justify-self-center w-full grid grid-cols-1 rounded-[2rem] shadow-[inset_0_0_2px_1px_#ffffff4d] ring-1 ring-black/5 max-lg:mx-auto max-lg:w-full max-lg:max-w-md max-w-lg'>
|
||||||
|
@ -78,72 +86,66 @@ export function ProjectPricingPlan({
|
||||||
<div className='text-sm'>/ {interval}</div>
|
<div className='text-sm'>/ {interval}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{requestsLineItem && plan.slug !== 'free' && (
|
{requestsLineItem && !isFreePlan && (
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<h4 className='text-sm/6 font-medium'>Requests:</h4>
|
<h4 className='text-sm/6 font-medium'>Requests:</h4>
|
||||||
|
|
||||||
{requestsLineItem.billingScheme === 'per_unit' ? (
|
{requestsLineItem.billingScheme === 'per_unit' ? (
|
||||||
<div className='ml-2 flex flex-row items-center gap-2'>
|
<div className='ml-4 flex flex-row items-center gap-2'>
|
||||||
<div className='text-xl font-semibold leading-none py-2'>
|
<div className='text-xl font-semibold leading-none py-2'>
|
||||||
${pricingAmountToFixedString(requestsLineItem.unitAmount)}
|
${pricingAmountToFixedString(requestsLineItem.unitAmount)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-sm'>
|
<div className='text-sm'>
|
||||||
/{' '}
|
/ per{' '}
|
||||||
{requestsLineItem.transformQuantity
|
{requestsLineItem.transformQuantity
|
||||||
? `${requestsLineItem.transformQuantity.divideBy} ${plur('request', requestsLineItem.transformQuantity.divideBy)}`
|
? `${requestsLineItem.transformQuantity.divideBy} ${plur('request', requestsLineItem.transformQuantity.divideBy)}`
|
||||||
: 'request'}
|
: 'request'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : requestsLineItem.billingScheme === 'tiered' ? (
|
) : requestsLineItem.billingScheme === 'tiered' ? (
|
||||||
<div className='ml-2 flex flex-col gap-2'>
|
<div className='ml-4 flex flex-col gap-2'>
|
||||||
{requestsLineItem.tiers?.map((tier, index) => {
|
{requestsLineItem.tiers?.map((tier, index) => {
|
||||||
const isFirst = index === 0
|
const isFirst = index === 0
|
||||||
// const isLast = index >= requestsLineItem.tiers!.length - 1
|
|
||||||
const hasUnitAmount = tier.unitAmount !== undefined
|
const hasUnitAmount = tier.unitAmount !== undefined
|
||||||
// const hasFlatAmount = tier.flatAmount !== undefined
|
|
||||||
const isFree = hasUnitAmount
|
const isFree = hasUnitAmount
|
||||||
? // TODO: are these two mutually exclusive? check in stripe
|
? // TODO: are these two mutually exclusive? check in stripe
|
||||||
tier.unitAmount === 0
|
tier.unitAmount === 0
|
||||||
: tier.flatAmount === 0
|
: tier.flatAmount === 0
|
||||||
|
|
||||||
// TODO: improve `inf` label
|
const isTierInfinite = tier.upTo === 'inf'
|
||||||
const numLabel =
|
const numLabel =
|
||||||
tier.upTo === 'inf'
|
tier.upTo === 'inf'
|
||||||
? 'infinite requests'
|
? 'infinite requests'
|
||||||
: `${humanNumber(tier.upTo)} ${plur('request', tier.upTo)}`
|
: `${humanNumber(tier.upTo)} ${plur('request', tier.upTo)}`
|
||||||
|
const price = `$${pricingAmountToFixedString(
|
||||||
|
hasUnitAmount ? tier.unitAmount! : tier.flatAmount!
|
||||||
|
)}${hasUnitAmount ? ' per request' : ''}`
|
||||||
|
|
||||||
|
const numDesc = isFree
|
||||||
|
? isFirst
|
||||||
|
? isTierInfinite
|
||||||
|
? `FREE for all requests per ${interval}`
|
||||||
|
: `FREE for the first ${numLabel} per ${interval}`
|
||||||
|
: isTierInfinite
|
||||||
|
? `FREE for all requests after that per ${interval}`
|
||||||
|
: `FREE for requests up to ${numLabel} per ${interval}`
|
||||||
|
: isFirst
|
||||||
|
? isTierInfinite
|
||||||
|
? `${price} per ${interval}`
|
||||||
|
: `${price} for the first ${numLabel} per ${interval}`
|
||||||
|
: isTierInfinite
|
||||||
|
? `${price} after that per ${interval}`
|
||||||
|
: `${price} up to ${numLabel} per ${interval}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className=''>
|
<div
|
||||||
{isFree ? (
|
key={index}
|
||||||
isFirst ? (
|
className='flex flex-row items-center gap-2 text-sm text-secondary-foreground/80'
|
||||||
<div>
|
>
|
||||||
FREE for the first {numLabel} per {interval}
|
<CornerDownRightIcon className='size-4' />
|
||||||
</div>
|
|
||||||
) : (
|
<span>{numDesc}</span>
|
||||||
<div>
|
|
||||||
$
|
|
||||||
{pricingAmountToFixedString(
|
|
||||||
hasUnitAmount
|
|
||||||
? tier.unitAmount!
|
|
||||||
: tier.flatAmount!
|
|
||||||
)}{' '}
|
|
||||||
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
|
||||||
{numLabel}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
$
|
|
||||||
{pricingAmountToFixedString(
|
|
||||||
hasUnitAmount
|
|
||||||
? tier.unitAmount!
|
|
||||||
: tier.flatAmount!
|
|
||||||
)}{' '}
|
|
||||||
{hasUnitAmount ? `per request ` : ''}up to{' '}
|
|
||||||
{numLabel}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -154,38 +156,49 @@ export function ProjectPricingPlan({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isFreePlan && (
|
||||||
|
<p className='text-pretty text-sm text-secondary-foreground/80'>
|
||||||
|
Try before you buy. 100% free!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{requestsRateLimit?.enabled && (
|
||||||
|
<div className='flex flex-row items-center gap-2 text-sm text-secondary-foreground/80'>
|
||||||
|
{isFreePlan ? (
|
||||||
|
<ShieldMinusIcon aria-hidden className='size-4' />
|
||||||
|
) : (
|
||||||
|
<ShieldCheckIcon aria-hidden className='size-4' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{isFreePlan ? 'Limited' : 'Rate-limited'} to{' '}
|
||||||
|
{requestsRateLimit.limit} requests per{' '}
|
||||||
|
{getRateLimitIntervalLabel(requestsRateLimit.interval)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{plan.features && (
|
{plan.features && (
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<h4 className='text-sm/6 font-medium'>Features:</h4>
|
<h4 className='text-sm/6 font-medium'>Features:</h4>
|
||||||
|
|
||||||
<ul className='space-y-1'>
|
<ul className='ml-4 flex flex-col gap-2 list-disc'>
|
||||||
{plan.features.map((feature, index) => (
|
{plan.features.map((feature, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className='group flex flex-row items-start gap-2 text-sm/6 data-[disabled]:text-gray-400'
|
className='flex flex-row items-center gap-2 text-sm text-secondary-foreground/80'
|
||||||
>
|
>
|
||||||
<span className='inline-flex h-6 items-center'>
|
<PlusIcon aria-hidden className='size-4' />
|
||||||
<PlusIcon
|
|
||||||
aria-hidden='true'
|
|
||||||
className='size-4 fill-gray-400 group-data-[disabled]:fill-gray-300'
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{feature}
|
<span>{feature}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plan.slug === 'free' && (
|
|
||||||
<p className='text-pretty text-xs/5 text-gray-400'>
|
|
||||||
Try before you buy. 100% free!
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{requestsLineItem?.billingScheme === 'tiered' && (
|
{requestsLineItem?.billingScheme === 'tiered' && (
|
||||||
<p className='text-pretty text-xs/5 text-gray-400'>
|
<p className='text-pretty text-xs/5 text-muted-foreground'>
|
||||||
{requestsLineItem.tiersMode === 'graduated' ? (
|
{requestsLineItem.tiersMode === 'graduated' ? (
|
||||||
<>
|
<>
|
||||||
Requests pricing tiers use{' '}
|
Requests pricing tiers use{' '}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { type ClassValue, clsx } from 'clsx'
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
|
import prettyMs from 'pretty-ms'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export { default as humanNumber } from 'human-number'
|
export { default as humanNumber } from 'human-number'
|
||||||
|
@ -28,3 +29,39 @@ export function pricingAmountToFixedString(amount: number): string {
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRateLimitIntervalLabel(rateLimitInterval: number): string {
|
||||||
|
const label = prettyMs(rateLimitInterval * 1000, {
|
||||||
|
verbose: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (label === '1 second') {
|
||||||
|
return 'second'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === '1 minute') {
|
||||||
|
return 'minute'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === '1 hour') {
|
||||||
|
return 'hour'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === '1 day') {
|
||||||
|
return 'day'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === '1 week') {
|
||||||
|
return 'week'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === '1 month') {
|
||||||
|
return 'month'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label === '1 year') {
|
||||||
|
return 'year'
|
||||||
|
}
|
||||||
|
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
|
|
@ -890,6 +890,9 @@ importers:
|
||||||
posthog-js:
|
posthog-js:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 1.255.0
|
version: 1.255.0
|
||||||
|
pretty-ms:
|
||||||
|
specifier: ^9.2.0
|
||||||
|
version: 9.2.0
|
||||||
react:
|
react:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 19.1.0
|
version: 19.1.0
|
||||||
|
|
Ładowanie…
Reference in New Issue