From 6c8df98cfd187e3fa5a1761c8015766b413dfc31 Mon Sep 17 00:00:00 2001 From: Grant Date: Mon, 11 May 2026 17:57:47 -0500 Subject: [PATCH] =?UTF-8?q?v0.2.0:34=20=E2=80=94=20Buy=20page:=20pre-popul?= =?UTF-8?q?ate=20featured=20code=20in=20discount=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- licensing-service/src/api/buy_page.rs | 36 +++++++++++++++++++++++++++ startos/versions/v0.2.0.ts | 4 ++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/licensing-service/src/api/buy_page.rs b/licensing-service/src/api/buy_page.rs index 93f59c1..88f5d9f 100644 --- a/licensing-service/src/api/buy_page.rs +++ b/licensing-service/src/api/buy_page.rs @@ -685,6 +685,12 @@ footer.kfooter a:hover {{ color:var(--navy-900); }} // declaration below the call hits the temporal-dead-zone error and kills // every event handler on the page (including the form submit). 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'); }} @@ -734,10 +740,19 @@ footer.kfooter a:hover {{ color:var(--navy-900); }} }}); // Reset any active discount apply state — a different tier may not // 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) {{ appliedCode = null; setStatus(null); setPaidButton(); + if (autoAppliedFeatured) {{ + codeInput.value = ''; + autoAppliedFeatured = false; + }} }} // Reflect new base price in the cert card. For fiat-priced // 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'; priceTag.textContent = t.featured.label; 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 {{ priceStrike.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. + // 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() {{ + autoAppliedFeatured = false; if (appliedCode && codeInput.value.trim().toUpperCase() !== appliedCode.code.toUpperCase()) {{ appliedCode = null; resetPrice(); @@ -894,6 +927,9 @@ footer.kfooter a:hover {{ color:var(--navy-900); }} is_free: !!j.is_free, 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 if (j.kind === 'free_license' || j.final_price_sats === 0) {{ priceStrike.textContent = fmtNum(j.base_price_sats) + ' sats'; diff --git a/startos/versions/v0.2.0.ts b/startos/versions/v0.2.0.ts index 29817c6..2e10c54 100644 --- a/startos/versions/v0.2.0.ts +++ b/startos/versions/v0.2.0.ts @@ -58,6 +58,8 @@ const RELEASE_NOTES = [ // in RELEASE_NOTES above (the milestone). Subsequent revisions // append here. 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: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') export const v0_2_0 = VersionInfo.of({ - version: '0.2.0:33', + version: '0.2.0:34', releaseNotes: { en_US: ROUTINE_NOTES }, // No on-disk transformation needed — v0.2.0:0 is a label change. // SQLite-level migrations live separately under