Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 47 additions & 83 deletions apps/landing-page/app/_components/pricing-individual-plans.astro
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
---
import {
GO_PLAN,
GO_PLAN_SOLD_OUT,
PRICING_SNAPSHOT,
formatUsd,
type PlanTierConfig,
} from '../_lib/pricing';
import {
CREDIT_BONUS_PCT,
fillTemplate,
getCurrentPlanLabel,
getPricingContent,
type PlanTierId,
} from '../_lib/pricing-content';
Expand All @@ -17,14 +16,15 @@ import { localeFromPath } from '../i18n';

const locale = localeFromPath(Astro.url.pathname);
const content = getPricingContent(locale);
const currentPlanLabel = getCurrentPlanLabel(locale);
const L = content.labels;
const P = content.personal;
const tiers = PRICING_SNAPSHOT.tiers;
const catalogCounts = await getCatalogCounts(locale);
const skillsCount = catalogCounts.skills.toLocaleString('en-US');
const systemsCount = catalogCounts.systems.toLocaleString('en-US');

type TierId = 'go' | PlanTierId;
type TierId = 'free' | PlanTierId;
interface ModelItem {
name: string;
icon: string;
Expand Down Expand Up @@ -90,11 +90,11 @@ const renderCatalogLabel = (label: string) => fillTemplate(label, {
const apiKeyBenefit = renderCatalogLabel(P.bringYourOwnApiKey);

const tierCopy = {
go: content.go,
free: content.free,
...content.plans,
};
const tierTaglines: Record<TierId, string> = {
go: content.go.tagline,
free: content.free.tagline,
plus: content.plans.plus.tagline,
pro: content.plans.pro.tagline,
max: content.plans.max.tagline,
Expand All @@ -114,13 +114,7 @@ const paidTierBenefits = (tier: PlanTierId) => {
].map(renderCatalogLabel);
};
const tierBenefits: Record<TierId, string[]> = {
go: [
content.go.allowance,
P.goConcurrency,
fillTemplate(P.customDomains, { count: '5' }),
...content.go.features,
...content.plans.plus.features.slice(1),
].map(renderCatalogLabel),
free: [content.free.concurrency, ...content.free.features].map(renderCatalogLabel),
plus: paidTierBenefits('plus'),
pro: paidTierBenefits('pro'),
max: paidTierBenefits('max'),
Expand Down Expand Up @@ -165,45 +159,29 @@ function individualView(tier: PlanTierConfig) {
};
}

const goYearlyEquivalent = Math.round(GO_PLAN.yearly.priceUsd / 12);
const goMonthlyDiscount = Math.round(
(1 - GO_PLAN.monthly.introPriceUsd / GO_PLAN.monthly.priceUsd) * 100,
);
const goYearlyDiscount = Math.round(
(1 - goYearlyEquivalent / GO_PLAN.monthly.priceUsd) * 100,
);
const goYearlySave = GO_PLAN.monthly.priceUsd * 12 - GO_PLAN.yearly.priceUsd;
const goView = {
const freeView = {
monthly: {
amount: formatUsd(GO_PLAN.monthly.introPriceUsd).replace('$', ''),
strike: formatUsd(GO_PLAN.monthly.priceUsd),
discount: goMonthlyDiscount,
saving: compactSavings(
GO_PLAN.monthly.priceUsd - GO_PLAN.monthly.introPriceUsd,
'monthly',
),
billed: fillTemplate(L.monthlyRenewal, {
amountUsd: formatUsd(GO_PLAN.monthly.priceUsd),
}),
amount: '0',
strike: null,
discount: null,
saving: null,
billed: L.freeForever,
},
yearly: {
amount: formatUsd(goYearlyEquivalent).replace('$', ''),
strike: formatUsd(GO_PLAN.monthly.priceUsd),
discount: goYearlyDiscount,
saving: compactSavings(goYearlySave, 'yearly'),
billed: fillTemplate(L.yearlySubline, {
totalUsd: compactBillingTotal(GO_PLAN.yearly.priceUsd),
savingsUsd: formatUsd(goYearlySave),
}),
amount: '0',
strike: null,
discount: null,
saving: null,
billed: L.freeForever,
},
};

const cardData = [
{
tier: 'go' as const,
logo: null,
view: goView,
regularMonthlyPrice: GO_PLAN.monthly.priceUsd,
tier: 'free' as const,
logo: '/pricing/plan-free.svg',
view: freeView,
regularMonthlyPrice: 0,
},
...tiers.map((tier) => ({
tier: tier.tier as PlanTierId,
Expand All @@ -227,7 +205,7 @@ const cardData = [
const popular = cardPopularModels;
const flagship = tier === 'plus' ? plusFlagshipModels : flagshipModels;
const imageResolution = tier === 'plus' ? '1K' : tier === 'pro' ? '2K' : '4K';
const bonusPct = tier === 'go' ? null : CREDIT_BONUS_PCT[tier];
const bonusPct = tier === 'free' ? null : CREDIT_BONUS_PCT[tier];
return (
<article class:list={['pricing-card', 'decision-card', 'compact-tier-card', `plan-${tier}`, { 'new-plan-card recommended': tier === 'pro' || tier === 'max' }]} data-tier={tier}>
{tier === 'pro' && <div class="new-plan-ribbon"><span>{P.mostPopular}</span></div>}
Expand All @@ -236,19 +214,15 @@ const cardData = [
<header class="plan-top">
<div class="plan-title-row">
<div class="plan-name-with-status">
{logo ? (
<img class="plan-wordmark-image" src={logo} alt={tier.charAt(0).toUpperCase() + tier.slice(1)} />
) : (
<svg class="plan-wordmark-image plan-go-wordmark" viewBox="0 0 88 49" role="img" aria-label="Go">
<path d="M1 2.5h63c12 0 21.5 9.8 21.5 22S76 46.5 64 46.5H1" />
<path d="M29 17c-2.4-3.6-6.4-5.5-11.4-5.5C8.8 11.5 2 17 2 24.5s6.8 13 15.6 13c5.7 0 10-2.3 12.3-6.5v-7H17.5" />
<ellipse cx="50.5" cy="24.5" rx="12.5" ry="13" />
</svg>
)}
<img class="plan-wordmark-image" src={logo} alt={tier.charAt(0).toUpperCase() + tier.slice(1)} />
</div>
<div class="plan-badges">
<span class="discount-corner-badge" data-when="monthly" data-intro-only>{fillTemplate(L.firstMonthTag, { pct: String(view.monthly.discount) })}</span>
<span class="discount-corner-badge" data-when="yearly">{fillTemplate(L.yearlyDiscountTag, { pct: String(view.yearly.discount) })}</span>
{view.monthly.discount != null && (
<span class="discount-corner-badge" data-when="monthly" data-intro-only>{fillTemplate(L.firstMonthTag, { pct: String(view.monthly.discount) })}</span>
)}
{view.yearly.discount != null && (
<span class="discount-corner-badge" data-when="yearly">{fillTemplate(L.yearlyDiscountTag, { pct: String(view.yearly.discount) })}</span>
)}
</div>
</div>
<p>{tierTaglines[tier]}</p>
Expand All @@ -260,17 +234,17 @@ const cardData = [
data-monthly-price
data-intro-price={view.monthly.amount}
data-regular-price={formatUsd(regularMonthlyPrice).replace('$', '')}
>{view.monthly.amount}</strong><small>{L.perMonth}</small><del data-intro-only>{view.monthly.strike}</del>
>{view.monthly.amount}</strong><small>{L.perMonth}</small>{view.monthly.strike != null && <del data-intro-only>{view.monthly.strike}</del>}
</div>
<div class="compact-price-detail" data-when="monthly" data-intro-only>
<span class="price-savings-note">{fillTemplate(P.saveAmount, { amount: view.monthly.saving })}</span>
<div class:list={['compact-price-detail', { 'free-price-detail': tier === 'free' }]} data-when="monthly" data-intro-only>
{view.monthly.saving != null && <span class="price-savings-note">{fillTemplate(P.saveAmount, { amount: view.monthly.saving })}</span>}
<small>{view.monthly.billed}</small>
</div>
<div class="price" data-when="yearly">
<span>$</span><strong class="rolling-price-number">{view.yearly.amount}</strong><small>{L.perMonth}</small><del>{view.yearly.strike}</del>
<span>$</span><strong class="rolling-price-number">{view.yearly.amount}</strong><small>{L.perMonth}</small>{view.yearly.strike != null && <del>{view.yearly.strike}</del>}
</div>
<div class="compact-price-detail" data-when="yearly">
<span class="price-savings-note">{fillTemplate(P.saveAmount, { amount: view.yearly.saving })}</span>
<div class:list={['compact-price-detail', { 'free-price-detail': tier === 'free' }]} data-when="yearly">
{view.yearly.saving != null && <span class="price-savings-note">{fillTemplate(P.saveAmount, { amount: view.yearly.saving })}</span>}
<small>{view.yearly.billed}</small>
</div>

Expand All @@ -279,16 +253,16 @@ const cardData = [
data-pricing-cta
data-tier={tier}
data-plan-name={tier.charAt(0).toUpperCase() + tier.slice(1)}
data-default-label={tierCopy[tier].ctaLabel}
aria-disabled={tier === 'go' && GO_PLAN_SOLD_OUT ? 'true' : undefined}
tabindex={tier === 'go' && GO_PLAN_SOLD_OUT ? -1 : undefined}
data-subscription-disabled={tier === 'go' && GO_PLAN_SOLD_OUT ? '' : undefined}
data-default-label={tier === 'free' ? currentPlanLabel : tierCopy[tier].ctaLabel}
aria-disabled={tier === 'free' ? 'true' : undefined}
tabindex={tier === 'free' ? -1 : undefined}
data-subscription-disabled={tier === 'free' ? '' : undefined}
>
<span>{tierCopy[tier].ctaLabel}</span>
<span>{tier === 'free' ? currentPlanLabel : tierCopy[tier].ctaLabel}</span>
</a>

<div class="plan-feature-details">
<div class="plan-model-modules">
{tier !== 'free' && <div class="plan-model-modules">
<section class:list={['plan-model-module', 'popular-model-module', { 'recommended-model-module': tier === 'pro' || tier === 'max' }]} data-model-module>
<header>
<h4>{P.popularModels}</h4>
Expand All @@ -309,10 +283,9 @@ const cardData = [
</button>
</section>

<section class:list={['plan-model-module', { 'unavailable-model-module': tier === 'go', 'recommended-model-module': tier === 'pro' || tier === 'max' }]} data-model-module>
<section class:list={['plan-model-module', { 'recommended-model-module': tier === 'pro' || tier === 'max' }]} data-model-module>
<header>
<h4>{tier === 'go' ? P.flagshipModels : fillTemplate(P.flagshipModelCount, { count: String(flagship.length) })}</h4>
{tier === 'go' && <span>{P.noAccess}</span>}
<h4>{fillTemplate(P.flagshipModelCount, { count: String(flagship.length) })}</h4>
</header>
<ul>
{flagship.map((model) => (
Expand All @@ -326,19 +299,19 @@ const cardData = [
</button>
</section>

<section class:list={['plan-model-module', 'multimodal-model-module', { 'unavailable-model-module': tier === 'go', 'recommended-model-module': tier === 'pro' || tier === 'max' }]}>
<header><h4>{P.imageModels}</h4><span>{tier === 'go' ? P.noAccess : fillTemplate(P.modelCount, { count: '3' })}</span></header>
<section class:list={['plan-model-module', 'multimodal-model-module', { 'recommended-model-module': tier === 'pro' || tier === 'max' }]}>
<header><h4>{P.imageModels}</h4><span>{fillTemplate(P.modelCount, { count: '3' })}</span></header>
<ul>
{imageModels.map((model) => (
<li class="model-with-status">
<span class:list={['model-logo', model.iconClass]}><img src={model.icon} alt="" loading="lazy" /></span>
<span>{model.name}</span>
{tier !== 'go' && <em class="multimodal-status">{fillTemplate(P.upToResolution, { resolution: imageResolution })}</em>}
<em class="multimodal-status">{fillTemplate(P.upToResolution, { resolution: imageResolution })}</em>
</li>
))}
</ul>
</section>
</div>
</div>}

<ul class="plan-benefit-list">
{tierBenefits[tier].map((benefit, index) => (
Expand Down Expand Up @@ -519,15 +492,6 @@ const cardData = [
object-position: left center;
}

.plan-go-wordmark {
width: 54px;
fill: none;
stroke: #202020;
stroke-width: 5;
stroke-linecap: round;
stroke-linejoin: round;
}

.plan-max .plan-wordmark-image {
filter: invert(1) brightness(1.08);
}
Expand Down
46 changes: 34 additions & 12 deletions apps/landing-page/app/_lib/pricing-analytics-bridge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
GO_PLAN,
HOSTED_CLOUD_CONSOLE_DOMAINS,
PRICING_SNAPSHOT,
type BillingInterval,
Expand All @@ -8,9 +7,13 @@ import {
} from './pricing';

export type PricingBridgeSource = 'wallet' | 'dashboard';
export type PublicPlanTier = 'free' | Exclude<PlanTier, 'go'>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new free exposure cannot be ingested by the Vela endpoint this emitter targets. powerformer/vela's services/api/src/pricing-analytics.ts still validates planId with the four-value enum go | plus | pro | max, and the object schema is strict. exposureEvents() now includes free in the same array as the paid tiers, while postPricingBridgeEvents() sends that array as one request, so the unsupported item rejects the entire batch and drops the paid exposure analytics as well. Please coordinate a Vela schema/mapper update to accept free before shipping, or keep this bridge payload on the existing enum and add Free through a compatible endpoint or path.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

export type PublicPricingTierConfig = Omit<PlanTierConfig, 'tier'> & {
tier: PublicPlanTier;
};

export type PlanExposureInput = {
planId: PlanTier;
planId: PublicPlanTier;
billingInterval: BillingInterval;
priceUsd: string;
creditsGrantedUsd: string;
Expand Down Expand Up @@ -74,26 +77,36 @@ export type PricingBridgeEvent =
payload: PricingClickInput;
};

const goTier: PlanTierConfig = {
tier: GO_PLAN.tier,
const freeTier: PublicPricingTierConfig = {
tier: 'free',
rank: 0,
recommended: false,
monthly: {
priceUsd: GO_PLAN.monthly.priceUsd,
introPriceUsd: GO_PLAN.monthly.introPriceUsd,
priceUsd: 0,
introPriceUsd: 0,
grantUsd: 0,
},
yearly: {
priceUsd: GO_PLAN.yearly.priceUsd,
discountPct: 50,
priceUsd: 0,
discountPct: 0,
grantUsd: 0,
},
deployLimit: 0,
};

export const PERSONAL_PRICING_TIERS: readonly PlanTierConfig[] = [
goTier,
...PRICING_SNAPSHOT.tiers,
const paidPublicTiers = PRICING_SNAPSHOT.tiers.map(
(tier): PublicPricingTierConfig => {
if (tier.tier === 'go') {
throw new Error('Public pricing snapshot must not contain the Go plan');
}

return { ...tier, tier: tier.tier };
},
);

export const PERSONAL_PRICING_TIERS: readonly PublicPricingTierConfig[] = [
freeTier,
...paidPublicTiers,
];

const sourceOverrideKeys = [
Expand Down Expand Up @@ -187,6 +200,15 @@ function isPlanTier(value: unknown): value is PlanTier {
);
}

function isPublicPlanTier(value: unknown): value is PublicPlanTier {
return (
value === 'free' ||
value === 'plus' ||
value === 'pro' ||
value === 'max'
);
}

function isBillingInterval(value: unknown): value is BillingInterval {
return value === 'monthly' || value === 'yearly';
}
Expand All @@ -207,7 +229,7 @@ function sanitizedPlanPayload(value: unknown): PlanExposureInput | null {
if (!value || typeof value !== 'object') return null;
const payload = value as Record<string, unknown>;
if (
!isPlanTier(payload.planId) ||
!isPublicPlanTier(payload.planId) ||
!isBillingInterval(payload.billingInterval) ||
!isUsdAmount(payload.priceUsd) ||
!isUsdAmount(payload.creditsGrantedUsd) ||
Expand Down
Loading
Loading