v0.2.0:39 — Buy page: render tier card for single-public-policy products

Previously the tier picker gated on `policies.len() < 2` and returned
an empty string when a product had only one public policy. Buyers
saw just the price card + form — none of the entitlements, marketing
bullets, or description the operator had carefully authored on that
tier. Reported against the Recap product, which has 3 policies but
only Pro public; Pro's bullets were invisible to buyers.

Fixed:

- render_tier_picker gate flipped from `< 2` to `is_empty()`. A
  single public policy now renders a single tier card.
- New `.tiers-1` grid class: one centered column at ~480px max-width.
  Keeps the single card from stretching to the full 1040px container.
- `n` computation extends to handle 1 in the existing match arm.

The price card below the picker still renders unchanged for the
single-policy case — acts as the buy-confirmation summary. Operators
keeping most tiers private and only exposing one to buyers now get
the same rich tier-card render that multi-tier products always had.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Grant
2026-05-11 22:48:33 -05:00
parent 5c7d66dbb2
commit 1a14b9c2e3
2 changed files with 14 additions and 4 deletions
+11 -3
View File
@@ -305,6 +305,10 @@ h1 {{
1fr /* row 7: features (fills) */
auto; /* row 8: button */
}}
/* Single-policy products: one centered card at a comfortable width.
Wide enough to read entitlements + marketing bullets clearly without
stretching across the full container. */
.tiers-1 {{ grid-template-columns:minmax(0, 480px); justify-content:center; }}
.tiers-2 {{ grid-template-columns:repeat(2, 1fr); }}
.tiers-3 {{ grid-template-columns:repeat(3, 1fr); }}
.tiers-4 {{ grid-template-columns:repeat(2, 1fr); }}
@@ -1093,19 +1097,23 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
}
/// Build the server-rendered tier-picker HTML. Returns an empty string
/// when the product has fewer than 2 public policies (i.e., the existing
/// single-price view is sufficient).
/// only when the product has zero public policies (the bare price-card +
/// form fallback covers that case). For one public policy, we still
/// render a single tier card so the operator-configured entitlements
/// and marketing bullets surface — without this, single-tier products
/// showed only price + form, eating the operator's tier copy.
fn render_tier_picker(
policies: &[crate::models::Policy],
initial: &Option<crate::models::Policy>,
product: &crate::models::Product,
featured_by_policy: &std::collections::HashMap<String, crate::models::DiscountCode>,
) -> String {
if policies.len() < 2 {
if policies.is_empty() {
return String::new();
}
let n = policies.len().min(4);
let class_n = match n {
1 => "tiers-1",
2 => "tiers-2",
3 => "tiers-3",
_ => "tiers-4",