What changed, and why
The referral-credit reward (issue #96) shipped, but nothing in the UI told a player it existed. The only trace was a passive line in the account popover — +N from sharing — and that appears after a credit lands. So the feature was invisible exactly when it should pull you in: a player had no way to learn that sharing a run earns runs.
This PR is copy only. It adds a short, accurate note in the two places the sharer meets the mechanic — the share affordance and the runs balance — plus three tests that pin the wording. No logic, no data model, no API touched. Blast radius is two Vue templates and two test files (52 inserted lines, nothing removed).
| File | Change |
|---|---|
components/ShareControls.vue | earn note on the share CTA and the live public-link state |
components/RunsBalance.vue | always-visible earn hint in the account popover |
tests/unit/components/*.spec.ts | three tests asserting the copy renders |
The mechanic the copy has to match
Copy is only worth shipping if it's true. Here is the ground truth the wording is checked against, all server-authoritative (the client only triggers a claim, it never grants credit).
| Question | Answer | Where |
|---|---|---|
| Who earns? | the sharer (referrer), never the new player | referral.ts:123 |
| What triggers it? | a new player opens the /r/:token link and signs up with email (anonymous → email account) | referral.ts:140 |
| How much? | 1 run by default | referral.ts:47 |
| Guardrails | idempotent per email + per device, self-referral blocked, cap 25 | referral.ts:48 |
The default reward (1 run) and the qualifying check: anonymous or email-less → not credited.
server/utils/referral.ts · 209 lines
⋯ 45 lines hidden (lines 1–45)
⋯ 89 lines hidden (lines 49–137)
⋯ 68 lines hidden (lines 142–209)
RunsBalance — make the earn path discoverable first
The account popover is where a player looks to answer "how do I get more runs?" It already had a post-hoc tally (+N from sharing) gated on referralCredits > 0. That's the reward landing, not the invitation. The new hint is always shown, so the earn path is visible before any credit exists; the tally below it is untouched and still appears only once something has been earned. Together: the hint is the how, the tally is the what you've earned.
Help text (21–24), the always-on earn hint (26–31), then the unchanged earned tally (33–39).
components/RunsBalance.vue · 160 lines
⋯ 20 lines hidden (lines 1–20)
⋯ 121 lines hidden (lines 40–160)
Tests that pin the copy
Each new test mounts the real component and asserts the note renders with the reward in it — the discriminating check that only this copy layer satisfies, not coverage padding. ShareControls is checked in both states (private CTA and, with a stubbed link, the public state). RunsBalance is checked with referralCredits = 0 — proving the hint shows before anything is earned, which is the whole point of the change.
Two tests: the reward is named on the initial affordance and once public.
tests/unit/components/ShareControls.spec.ts · 118 lines
⋯ 89 lines hidden (lines 1–89)
⋯ 1 line hidden (lines 118–118)
The earn hint is present even when nothing has been earned from sharing.