diff --git a/apps/web/package.json b/apps/web/package.json index 77807cf5..8b741a3e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -59,6 +59,7 @@ "next-themes": "catalog:", "plur": "catalog:", "posthog-js": "catalog:", + "pretty-ms": "^9.2.0", "react": "catalog:", "react-dom": "catalog:", "react-infinite-scroll-hook": "catalog:", diff --git a/apps/web/src/app/marketplace/projects/[namespace]/[project-slug]/marketplace-public-project-detail.tsx b/apps/web/src/app/marketplace/projects/[namespace]/[project-slug]/marketplace-public-project-detail.tsx index 965c7c9c..2f728fee 100644 --- a/apps/web/src/app/marketplace/projects/[namespace]/[project-slug]/marketplace-public-project-detail.tsx +++ b/apps/web/src/app/marketplace/projects/[namespace]/[project-slug]/marketplace-public-project-detail.tsx @@ -575,7 +575,7 @@ function ProjectHeader({ = { // day: 'daily', @@ -41,10 +50,10 @@ export function ProjectPricingPlan({ const requestsLineItem = lineItems.find( (lineItem) => lineItem.slug === 'requests' ) + const isFreePlan = plan.slug === 'free' - // TODO: rate-limits - // const deployment = project.lastPublishedDeployment - // const requestsRateLimit = plan.rateLimit ?? deployment?.defaultRateLimit + const deployment = project.lastPublishedDeployment + const requestsRateLimit = plan.rateLimit ?? deployment?.defaultRateLimit // TODO: support custom line-items // const customLineItems = lineItems.find( @@ -53,8 +62,7 @@ export function ProjectPricingPlan({ // TODO: support defaultAggregation // TODO: support trialPeriodDays - - // TODO: add rate-limits and finesse free tier to not be so bare-bones + // TODO: highlight if any tools are disabled on this pricing plan return (
@@ -78,72 +86,66 @@ export function ProjectPricingPlan({
/ {interval}
- {requestsLineItem && plan.slug !== 'free' && ( + {requestsLineItem && !isFreePlan && (

Requests:

{requestsLineItem.billingScheme === 'per_unit' ? ( -
+
${pricingAmountToFixedString(requestsLineItem.unitAmount)}
- /{' '} + / per{' '} {requestsLineItem.transformQuantity ? `${requestsLineItem.transformQuantity.divideBy} ${plur('request', requestsLineItem.transformQuantity.divideBy)}` : 'request'}
) : requestsLineItem.billingScheme === 'tiered' ? ( -
+
{requestsLineItem.tiers?.map((tier, index) => { const isFirst = index === 0 - // const isLast = index >= requestsLineItem.tiers!.length - 1 const hasUnitAmount = tier.unitAmount !== undefined - // const hasFlatAmount = tier.flatAmount !== undefined const isFree = hasUnitAmount ? // TODO: are these two mutually exclusive? check in stripe tier.unitAmount === 0 : tier.flatAmount === 0 - // TODO: improve `inf` label + const isTierInfinite = tier.upTo === 'inf' const numLabel = tier.upTo === 'inf' ? 'infinite requests' : `${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 ( -
- {isFree ? ( - isFirst ? ( -
- FREE for the first {numLabel} per {interval} -
- ) : ( -
- $ - {pricingAmountToFixedString( - hasUnitAmount - ? tier.unitAmount! - : tier.flatAmount! - )}{' '} - {hasUnitAmount ? `per request ` : ''}up to{' '} - {numLabel} -
- ) - ) : ( -
- $ - {pricingAmountToFixedString( - hasUnitAmount - ? tier.unitAmount! - : tier.flatAmount! - )}{' '} - {hasUnitAmount ? `per request ` : ''}up to{' '} - {numLabel} -
- )} +
+ + + {numDesc}
) })} @@ -154,38 +156,49 @@ export function ProjectPricingPlan({
)} + {isFreePlan && ( +

+ Try before you buy. 100% free! +

+ )} + + {requestsRateLimit?.enabled && ( +
+ {isFreePlan ? ( + + ) : ( + + )} + + + {isFreePlan ? 'Limited' : 'Rate-limited'} to{' '} + {requestsRateLimit.limit} requests per{' '} + {getRateLimitIntervalLabel(requestsRateLimit.interval)} + +
+ )} + {plan.features && (

Features:

-
    +
      {plan.features.map((feature, index) => (
    • - - + - {feature} + {feature}
    • ))}
)} - {plan.slug === 'free' && ( -

- Try before you buy. 100% free! -

- )} - {requestsLineItem?.billingScheme === 'tiered' && ( -

+

{requestsLineItem.tiersMode === 'graduated' ? ( <> Requests pricing tiers use{' '} diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index b4f71ba4..a5cd0b2f 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -1,4 +1,5 @@ import { type ClassValue, clsx } from 'clsx' +import prettyMs from 'pretty-ms' import { twMerge } from 'tailwind-merge' export { default as humanNumber } from 'human-number' @@ -28,3 +29,39 @@ export function pricingAmountToFixedString(amount: number): string { 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 +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a1f3f08..9a1ebbfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -890,6 +890,9 @@ importers: posthog-js: specifier: 'catalog:' version: 1.255.0 + pretty-ms: + specifier: ^9.2.0 + version: 9.2.0 react: specifier: 'catalog:' version: 19.1.0