pull/720/head
Travis Fischer 2025-07-05 13:26:43 -07:00
rodzic c88bc60945
commit 02d841aa54
4 zmienionych plików z 122 dodań i 64 usunięć

Wyświetl plik

@ -17,7 +17,8 @@
"start": "next start", "start": "next start",
"clean": "del .next", "clean": "del .next",
"test": "run-s test:*", "test": "run-s test:*",
"test:lint": "next lint" "test:lint": "next lint",
"test:unit": "vitest run"
}, },
"dependencies": { "dependencies": {
"@agentic/platform": "workspace:*", "@agentic/platform": "workspace:*",

Wyświetl plik

@ -6,9 +6,11 @@ import type {
} 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 { Loader2Icon, PlusIcon } from 'lucide-react'
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'
// const intervalToLabelMap: Record<PricingInterval, string> = { // const intervalToLabelMap: Record<PricingInterval, string> = {
// day: 'daily', // day: 'daily',
@ -39,15 +41,14 @@ export function ProjectPricingPlan({
const requestsLineItem = lineItems.find( const requestsLineItem = lineItems.find(
(lineItem) => lineItem.slug === 'requests' (lineItem) => lineItem.slug === 'requests'
) )
// TODO // TODO: support custom line-items
// const customLineItems = lineItems.find( // const customLineItems = lineItems.find(
// (lineItem) => lineItem.slug !== 'base' && lineItem.slug !== 'requests' // (lineItem) => lineItem.slug !== 'base' && lineItem.slug !== 'requests'
// ) // )
// TODO: support transformQuantity.divideBy
// TODO: support defaultAggregation // TODO: support defaultAggregation
// TODO: toFixed(2) doesn't work for usage-based pricing with small amounts // TODO: add rate-limits and finesse free tier to not be so basic
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'>
@ -62,7 +63,10 @@ export function ProjectPricingPlan({
<div className='flex flex-row items-center gap-2'> <div className='flex flex-row items-center gap-2'>
<div className='text-4xl font-semibold text-gray-950 leading-none py-2'> <div className='text-4xl font-semibold text-gray-950 leading-none py-2'>
${baseLineItem ? (baseLineItem.amount / 100).toFixed(2) : 0} $
{baseLineItem
? pricingAmountToFixedString(baseLineItem.amount)
: 0}
</div> </div>
<div className='text-sm text-gray-600'>/ {interval}</div> <div className='text-sm text-gray-600'>/ {interval}</div>
@ -77,10 +81,7 @@ export function ProjectPricingPlan({
{requestsLineItem.billingScheme === 'per_unit' ? ( {requestsLineItem.billingScheme === 'per_unit' ? (
<div className='flex flex-row items-center gap-2'> <div className='flex flex-row items-center gap-2'>
<div className='text-xl font-semibold text-gray-950 leading-none py-2'> <div className='text-xl font-semibold text-gray-950 leading-none py-2'>
$ ${pricingAmountToFixedString(requestsLineItem.unitAmount)}
{requestsLineItem.unitAmount <= 0
? 0
: (requestsLineItem.unitAmount / 100).toFixed(2)}
</div> </div>
<div className='text-sm text-gray-600'> <div className='text-sm text-gray-600'>
@ -91,68 +92,58 @@ export function ProjectPricingPlan({
</div> </div>
</div> </div>
) : requestsLineItem.billingScheme === 'tiered' ? ( ) : requestsLineItem.billingScheme === 'tiered' ? (
requestsLineItem.tiersMode === 'graduated' ? ( <div>
<div>TODO</div> {requestsLineItem.tiers?.map((tier, index) => {
) : requestsLineItem.tiersMode === 'volume' ? ( const isFirst = index === 0
<div> // const isLast = index >= requestsLineItem.tiers!.length - 1
{requestsLineItem.tiers?.map((tier, index) => { const hasUnitAmount = tier.unitAmount !== undefined
const isFirst = index === 0 // const hasFlatAmount = tier.flatAmount !== undefined
// const isLast = index >= requestsLineItem.tiers!.length - 1 const isFree = hasUnitAmount
const hasUnitAmount = tier.unitAmount !== undefined ? // TODO: are these two mutually exclusive? check in stripe
// const hasFlatAmount = tier.flatAmount !== undefined tier.unitAmount === 0
const isFree = hasUnitAmount : tier.flatAmount === 0
? // TODO: are these two mutually exclusive? check in stripe
tier.unitAmount === 0
: tier.flatAmount === 0
// TODO: improve `inf` label // TODO: improve `inf` label
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)}`
return ( return (
<div key={index} className=''> <div key={index} className=''>
{isFree ? ( {isFree ? (
isFirst ? ( isFirst ? (
<div>FREE for the first {numLabel}</div> <div>
) : ( FREE for the first {numLabel} per {interval}
<div> </div>
$
{hasUnitAmount
? (tier.unitAmount! / 100)
.toFixed(10)
.replace(/0+$/, '')
: (tier.flatAmount! / 100)
.toFixed(10)
.replace(/0+$/, '')}{' '}
{hasUnitAmount ? `per request ` : ''}up to{' '}
{numLabel}
</div>
)
) : ( ) : (
<div> <div>
$ $
{hasUnitAmount {pricingAmountToFixedString(
? (tier.unitAmount! / 100) hasUnitAmount
.toFixed(10) ? tier.unitAmount!
.replace(/0+$/, '') : tier.flatAmount!
: (tier.flatAmount! / 100) )}{' '}
.toFixed(10)
.replace(/0+$/, '')}{' '}
{hasUnitAmount ? `per request ` : ''}up to{' '} {hasUnitAmount ? `per request ` : ''}up to{' '}
{numLabel} {numLabel}
</div> </div>
)} )
</div> ) : (
) <div>
})} $
</div> {pricingAmountToFixedString(
) : ( hasUnitAmount
<div> ? tier.unitAmount!
Unsupported pricing config. Please contact support. : tier.flatAmount!
</div> )}{' '}
) {hasUnitAmount ? `per request ` : ''}up to{' '}
{numLabel}
</div>
)}
</div>
)
})}
</div>
) : ( ) : (
<div>Unsupported pricing config. Please contact support.</div> <div>Unsupported pricing config. Please contact support.</div>
)} )}
@ -169,7 +160,7 @@ export function ProjectPricingPlan({
{plan.features.map((feature, index) => ( {plan.features.map((feature, index) => (
<li <li
key={index} key={index}
className='group flex flex-row items-start gap-3 text-sm/6 text-gray-600 data-[disabled]:text-gray-400' className='group flex flex-row items-start gap-2 text-sm/6 text-gray-600 data-[disabled]:text-gray-400'
> >
<span className='inline-flex h-6 items-center'> <span className='inline-flex h-6 items-center'>
<PlusIcon <PlusIcon
@ -184,6 +175,38 @@ export function ProjectPricingPlan({
</ul> </ul>
</div> </div>
)} )}
{requestsLineItem?.billingScheme === 'tiered' && (
<p className='text-pretty text-xs/5 text-gray-400'>
{requestsLineItem.tiersMode === 'graduated' ? (
<>
Requests pricing tiers use{' '}
<Link
href='https://docs.stripe.com/products-prices/pricing-models#graduated-pricing'
className='link'
target='_blank'
rel='noreferrer'
>
graduated pricing
</Link>
.
</>
) : (
<>
Requests pricing tiers use{' '}
<Link
href='https://docs.stripe.com/products-prices/pricing-models#volume-based-pricing'
className='link'
target='_blank'
rel='noreferrer'
>
volume-based pricing
</Link>
.
</>
)}
</p>
)}
</div> </div>
<Button <Button

Wyświetl plik

@ -0,0 +1,16 @@
import { expect, test } from 'vitest'
import { pricingAmountToFixedString } from './utils'
test('pricingAmountToFixedString', () => {
expect(pricingAmountToFixedString(0.008)).toBe('0.00008')
expect(pricingAmountToFixedString(200)).toBe('2.00')
expect(pricingAmountToFixedString(52_399)).toBe('523.99')
expect(pricingAmountToFixedString(0.0)).toBe('0')
expect(pricingAmountToFixedString(0)).toBe('0')
expect(pricingAmountToFixedString(0.000_000_000_000_1)).toBe('0')
expect(pricingAmountToFixedString(0.01)).toBe('0.0001')
expect(pricingAmountToFixedString(10)).toBe('0.10')
expect(pricingAmountToFixedString(390)).toBe('3.90')
expect(pricingAmountToFixedString(1)).toBe('0.01')
})

Wyświetl plik

@ -10,3 +10,21 @@ export function cn(...inputs: ClassValue[]) {
export function randomInRange(min: number, max: number) { export function randomInRange(min: number, max: number) {
return Math.random() * (max - min) + min return Math.random() * (max - min) + min
} }
export function pricingAmountToFixedString(amount: number): string {
const output = (amount / 100)
// Cap the precision to 10 because of floating point issues
// (could remove this constraint in the future by not dividing by 100
// and handling the precision manually if needed)
.toFixed(10)
.replace(/0+$/, '')
.replace(/\.0*$/, '.00')
// (not a $ sign, but a substitution $1)
.replace(/(\.\d)$/, '$10')
if (output === '0.00') {
return '0'
}
return output
}