v0.2.0:35 — Free tiers render as "Free" on the buy-page tier card

Previously the server rendered a 0-priced tier as "0 sats" (or
"0.00 USD") in the tier-card headline. The price card below the
tier picker already swapped to "FREE" via the JS path, so the
two surfaces disagreed.

Now: when post-discount price is 0, the tier card renders the
headline as "Free" with no unit suffix and no cadence-suffix
("Free /yr" would be incoherent). recurring_meta ("Renews
annually") still surfaces beneath for recurring-free edge cases,
so cadence isn't lost — just not stuffed into the headline.

Cosmetic; no API or schema change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Grant
2026-05-11 18:15:28 -05:00
parent 6c8df98cfd
commit a0995c9c31
2 changed files with 18 additions and 2 deletions
+15 -1
View File
@@ -1133,7 +1133,15 @@ fn render_tier_picker(
.map(|c| crate::api::purchase::compute_discount(&c.kind, c.amount, base_price_units)) .map(|c| crate::api::purchase::compute_discount(&c.kind, c.amount, base_price_units))
.unwrap_or(0); .unwrap_or(0);
let post_discount_units = (base_price_units - discount_units).max(0); let post_discount_units = (base_price_units - discount_units).max(0);
let (price_fmt, price_unit) = if product.price_currency == "SAT" { // Render free tiers as "Free" rather than "0 sats" / "0.00
// USD". Matches the price card below the tier picker, which
// already swaps the price + unit to "FREE" via the JS path.
// Cadence suffix is suppressed below as well, since a
// "Free / yr" suffix would be incoherent.
let is_free = post_discount_units == 0;
let (price_fmt, price_unit) = if is_free {
("Free".to_string(), String::new())
} else if product.price_currency == "SAT" {
(format_thousands(post_discount_units), "sats".to_string()) (format_thousands(post_discount_units), "sats".to_string())
} else { } else {
let cents = post_discount_units; let cents = post_discount_units;
@@ -1395,6 +1403,12 @@ fn render_tier_picker(
"<div class=\"tier-meta-block\">{}{}{}{}</div>", "<div class=\"tier-meta-block\">{}{}{}{}</div>",
dur_html, recurring_meta, trial_banner, trial_meta dur_html, recurring_meta, trial_banner, trial_meta
); );
// Suppress the cadence suffix on free tiers so the price
// doesn't render as "Free /yr" or similar. The "Free"
// string alone is enough; the recurring_meta line below
// (e.g. "Renews annually") still surfaces the cadence for
// recurring tiers that aren't free.
let cadence_suffix = if is_free { "" } else { cadence_suffix };
format!( format!(
r#"<div class="{classes}" data-policy-slug="{slug}">{popular_pill}{featured_ribbon}{launch_meta_html}<div class="tier-name">{name}</div>{original_price_html}<div class="tier-price">{price_fmt}<span class="tier-price-unit">{price_unit}{cadence_suffix}</span></div>{meta_block_html}{description_html}{features_html}<button type="button" class="tier-select-btn">Select</button></div>"#, r#"<div class="{classes}" data-policy-slug="{slug}">{popular_pill}{featured_ribbon}{launch_meta_html}<div class="tier-name">{name}</div>{original_price_html}<div class="tier-price">{price_fmt}<span class="tier-price-unit">{price_unit}{cadence_suffix}</span></div>{meta_block_html}{description_html}{features_html}<button type="button" class="tier-select-btn">Select</button></div>"#,
classes = classes, classes = classes,
+3 -1
View File
@@ -58,6 +58,8 @@ const RELEASE_NOTES = [
// in RELEASE_NOTES above (the milestone). Subsequent revisions // in RELEASE_NOTES above (the milestone). Subsequent revisions
// append here. // append here.
const ROUTINE_NOTES = [ const ROUTINE_NOTES = [
'0.2.0:35 — **Free tiers render as "Free" on the buy page tier card.** Previously the server rendered a 0-priced tier as `0 sats` (or `0.00 USD`) in the tier-card price headline, even though the price card BELOW the picker already swapped to `FREE` via the JS path. Inconsistency fixed at the server-render layer: when post-discount price is 0, the tier card renders the headline as `Free` with no unit suffix and no cadence (`Free /yr` would be incoherent). Recurring-meta line ("Renews annually") still surfaces beneath for recurring tiers that happen to be free, so the cadence is still visible — just not stuffed into the headline. Cosmetic; no API or schema change.',
'',
'0.2.0:34 — **Buy page: featured discount pre-populates the code field and shows "Launch special applied" on load.** Previously the launch-special discount auto-applied silently at payment time but the discount-code input was empty, leaving buyers unsure whether they needed to type anything. Now: when a tier has an active featured code, the input renders pre-filled with the code (e.g. `LAUNCH`) and the green "Launch special applied" status badge shows on load. The price card has always rendered the slashed-original + discounted-current price; this change just makes the form match. Tier switches clear an auto-populated code so a code that doesn\'t apply to the new tier doesn\'t linger; buyer-typed codes are untouched. UI / JS only; no API or schema change.', '0.2.0:34 — **Buy page: featured discount pre-populates the code field and shows "Launch special applied" on load.** Previously the launch-special discount auto-applied silently at payment time but the discount-code input was empty, leaving buyers unsure whether they needed to type anything. Now: when a tier has an active featured code, the input renders pre-filled with the code (e.g. `LAUNCH`) and the green "Launch special applied" status badge shows on load. The price card has always rendered the slashed-original + discounted-current price; this change just makes the form match. Tier switches clear an auto-populated code so a code that doesn\'t apply to the new tier doesn\'t linger; buyer-typed codes are untouched. UI / JS only; no API or schema change.',
'', '',
'0.2.0:33 — **Drop a long-standing unused-variable warning.** Removed `let invoice_id_safe = html_escape(&invoice_id);` in `src/api/mod.rs` — the value was computed but never referenced anywhere in the thank-you-page template (the HTML uses `invoice_id_json` for the inline JS, and the on-screen invoice id renders from JS via that JSON variable). One-line cleanup; `cargo check` is now warning-free. No behavior change.', '0.2.0:33 — **Drop a long-standing unused-variable warning.** Removed `let invoice_id_safe = html_escape(&invoice_id);` in `src/api/mod.rs` — the value was computed but never referenced anywhere in the thank-you-page template (the HTML uses `invoice_id_json` for the inline JS, and the on-screen invoice id renders from JS via that JSON variable). One-line cleanup; `cargo check` is now warning-free. No behavior change.',
@@ -493,7 +495,7 @@ const ROUTINE_NOTES = [
].join('\n\n') ].join('\n\n')
export const v0_2_0 = VersionInfo.of({ export const v0_2_0 = VersionInfo.of({
version: '0.2.0:34', version: '0.2.0:35',
releaseNotes: { en_US: ROUTINE_NOTES }, releaseNotes: { en_US: ROUTINE_NOTES },
// No on-disk transformation needed — v0.2.0:0 is a label change. // No on-disk transformation needed — v0.2.0:0 is a label change.
// SQLite-level migrations live separately under // SQLite-level migrations live separately under