Skip to content

fix(web): keep the discount code until the upgrade actually completes - #1568

Open
rajarshidattapy wants to merge 1 commit into
supermemoryai:mainfrom
rajarshidattapy:fix/promo-code-clear-on-upgrade
Open

fix(web): keep the discount code until the upgrade actually completes#1568
rajarshidattapy wants to merge 1 commit into
supermemoryai:mainfrom
rajarshidattapy:fix/promo-code-clear-on-upgrade

Conversation

@rajarshidattapy

Copy link
Copy Markdown
Contributor

Relates to #1546 — see the note at the bottom, the reported bug is the opposite of the real one.

Problem

All eight checkout entry points cleared the stored promo code the instant autumn.attach() resolved:

const result = await autumn.attach({ planId, discounts: promoCode.getDiscounts(), successUrl })
promoCode.clear()
if (result?.paymentUrl) { window.location.href = result.paymentUrl; return }

attach() resolving only means Stripe handed back a payment URL — the user has not paid yet. Close that tab, hit back, or let the card decline, and the code is already gone from localStorage; ?discountCode= was stripped from the URL on capture, so there is no way to recover it short of finding the original link again.

Fix

Clear centrally, on the signal that actually means "this checkout went through": the org moving up a plan. PromoCodeHost already mounts on every app page and now reads the plan from useTokenUsage, so the eight promoCode.clear() calls are gone and usePromoCode() is down to { getDiscounts }.

  • Stored codes carry the plan they were captured against; PLAN_RANK[current] > PLAN_RANK[stored] spends them.
  • Downgrades and trial expiries deliberately do not spend an unused code.
  • Nothing is stamped while autumn.isLoading — recording a placeholder free would spend the code on the next render.

Codes now also carry a 30-day expiry. Without the eager clear, a code that never gets redeemed would otherwise sit in localStorage discounting every future checkout indefinitely, which is the leak #1546 was worried about. The expiry bounds it.

Storage moves from a bare string to JSON. Bare values written by #1523 still work and get stamped with a plan and expiry on the next mount.

Tests

apps/web/hooks/use-promo-code.test.ts (bun:test, 8 cases) covers the two pure pieces: parseStoredPromoCode (JSON, expired, legacy bare code, malformed) and isPromoCodeSpent (upgrade spends, unfinished checkout does not, downgrade/trial-expiry does not, unstamped code never does).

tsc --noEmit on apps/web reports the same 84 pre-existing errors as main; biome ci --changed is clean.

On issue #1546

The issue reports that usePromoCode().clear has zero call sites, so a code applies to every checkout forever. That is not the case on main — all eight sites call promoCode.clear(), and have since #1523 introduced the feature; the issue's grep only captured the discounts: lines. The real defect in that code is the inverse, which is what this PR fixes. The permanent-toast half of the report follows from the same mistaken premise: the toast is dismissed on clear.

Every checkout call site cleared the stored promo code as soon as
`autumn.attach()` resolved, but attach resolving only means Stripe handed back
a payment URL. A user who closed or abandoned that checkout page came back with
the code already gone from localStorage and the `?discountCode=` param long
since stripped, with no way to get it back.

Clear centrally instead: `PromoCodeHost` spends the code once the org moves up
a plan, which is the point at which the checkout it was captured for actually
went through. Downgrades and trial expiries leave an unused code alone. Codes
also carry a 30-day expiry now, so one that is never redeemed stops applying to
future checkouts instead of discounting them forever.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant