v0.2.0:34 — Buy page: pre-populate featured code in discount input

Previously a tier's featured (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 to claim
the slashed price.

Now: when a tier has an active featured discount, selectTier()
pre-fills codeInput with the code string and flips into the
"applied" state — appliedCode set, status badge shows "Launch
special applied". The price card has always rendered the
struck-original + discounted-current price; this change just makes
the form match what's already visually claimed.

New `autoAppliedFeatured` flag distinguishes auto-populated codes
from buyer-typed ones:
- On tier switch, the reset block also clears the input when
  autoAppliedFeatured was true (the prior featured code doesn't
  necessarily apply to the new tier; better to start fresh).
- Buyer-typed codes are NOT cleared on tier switch — they may be
  valid for the new tier, and the buyer can hit Apply to check.
- Any keystroke in codeInput, or a successful manual Apply, flips
  the flag to false.

JS / template only; 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 17:57:47 -05:00
parent 752beff429
commit 6c8df98cfd
2 changed files with 39 additions and 1 deletions
+36
View File
@@ -685,6 +685,12 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
// declaration below the call hits the temporal-dead-zone error and kills // declaration below the call hits the temporal-dead-zone error and kills
// every event handler on the page (including the form submit). // every event handler on the page (including the form submit).
let appliedCode = null; // {{ code, kind, is_free, final_price_sats }} let appliedCode = null; // {{ code, kind, is_free, final_price_sats }}
// `true` when the currently-applied code was auto-populated from a
// tier's featured (launch-special) discount, vs. typed by the buyer
// and confirmed via the Apply button. Tracked separately so that
// tier switches can clear the auto-populated value from the input
// without wiping a code the buyer deliberately typed.
let autoAppliedFeatured = false;
function fmtSats(n) {{ return Number(n).toLocaleString('en-US'); }} function fmtSats(n) {{ return Number(n).toLocaleString('en-US'); }}
@@ -734,10 +740,19 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
}}); }});
// Reset any active discount apply state — a different tier may not // Reset any active discount apply state — a different tier may not
// honor the same code (server validates again on the next Apply). // honor the same code (server validates again on the next Apply).
// If the previously-applied code was auto-populated (a featured
// discount on the prior tier), also clear it from the input so we
// don't carry stale auto-text into a tier that doesn't honor it.
// Buyer-typed codes are NOT cleared from the input — they may be
// valid for the new tier and the buyer can hit Apply to check.
if (appliedCode) {{ if (appliedCode) {{
appliedCode = null; appliedCode = null;
setStatus(null); setStatus(null);
setPaidButton(); setPaidButton();
if (autoAppliedFeatured) {{
codeInput.value = '';
autoAppliedFeatured = false;
}}
}} }}
// Reflect new base price in the cert card. For fiat-priced // Reflect new base price in the cert card. For fiat-priced
// products the unit cell ("sats" → "USD" / "EUR") also swaps. // products the unit cell ("sats" → "USD" / "EUR") also swaps.
@@ -754,6 +769,20 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
priceStrike.style.display = 'block'; priceStrike.style.display = 'block';
priceTag.textContent = t.featured.label; priceTag.textContent = t.featured.label;
priceTag.style.display = 'inline-block'; priceTag.style.display = 'inline-block';
// Pre-populate the discount input + flip into the "applied"
// state so the buyer sees what's been applied without having
// to type it. Marked `autoAppliedFeatured = true` so the
// selectTier reset above will clear the input on tier switch
// (in case the new tier doesn't honor a featured code).
codeInput.value = t.featured.code;
appliedCode = {{
code: t.featured.code,
kind: t.featured.kind,
is_free: (t.featured.kind === 'free_license' || t.featured.discounted_price_sats === 0),
final_price_sats: t.featured.discounted_price_sats,
}};
autoAppliedFeatured = true;
setStatus('ok', 'Launch special applied.');
}} else {{ }} else {{
priceStrike.style.display = 'none'; priceStrike.style.display = 'none';
priceTag.style.display = 'none'; priceTag.style.display = 'none';
@@ -851,7 +880,11 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
}} }}
// Reset apply state if the buyer edits the code after a successful Apply. // Reset apply state if the buyer edits the code after a successful Apply.
// Any keystroke also strips the "auto-populated" flag — once the buyer
// touches the input, the value is theirs (not ours to clear on a tier
// switch).
codeInput.addEventListener('input', function() {{ codeInput.addEventListener('input', function() {{
autoAppliedFeatured = false;
if (appliedCode && codeInput.value.trim().toUpperCase() !== appliedCode.code.toUpperCase()) {{ if (appliedCode && codeInput.value.trim().toUpperCase() !== appliedCode.code.toUpperCase()) {{
appliedCode = null; appliedCode = null;
resetPrice(); resetPrice();
@@ -894,6 +927,9 @@ footer.kfooter a:hover {{ color:var(--navy-900); }}
is_free: !!j.is_free, is_free: !!j.is_free,
final_price_sats: j.final_price_sats, final_price_sats: j.final_price_sats,
}}; }};
// Buyer typed + clicked Apply — this code is theirs, not ours
// to clear on a tier switch.
autoAppliedFeatured = false;
// Update price card // Update price card
if (j.kind === 'free_license' || j.final_price_sats === 0) {{ if (j.kind === 'free_license' || j.final_price_sats === 0) {{
priceStrike.textContent = fmtNum(j.base_price_sats) + ' sats'; priceStrike.textContent = fmtNum(j.base_price_sats) + ' sats';
+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: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.',
'', '',
'0.2.0:32 — **Per-product policy cap also pre-checked + grandfathered.** Extends the v0.2.0:31 cap-handling pattern to the third tier-enforced surface (Creator caps each product at 5 policies). Same shape, just scoped to a single product instead of the whole instance.', '0.2.0:32 — **Per-product policy cap also pre-checked + grandfathered.** Extends the v0.2.0:31 cap-handling pattern to the third tier-enforced surface (Creator caps each product at 5 policies). Same shape, just scoped to a single product instead of the whole instance.',
@@ -491,7 +493,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:33', version: '0.2.0:34',
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