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:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user