feat(ux): scan-to-cash-out, first-run walkthrough, plain language

Cashing out was the least approachable thing here: open your wallet,
create an invoice for exactly the right amount, copy it, come back, paste
it. That is the step people abandon, leaving sats behind.

LNURL-withdraw replaces it with a scan. The arcade shows a code, the
wallet pulls the funds, and the player never handles an invoice or types
an amount. The paste path is kept for wallets without LNURL support, but
folded away.

The withdraw token is a bearer instrument, so it is random, single-use,
bound to one account and one amount, and expires in five minutes. Sixteen
goroutines racing one code yield exactly one payment. Funds are debited
when the code is issued — otherwise a player could cash out and bet the
same sats before the wallet claimed them — and a sweep refunds any code
that is never scanned.

bech32 is verified against the BIP-173 vectors, including the invalid
ones. Getting this wrong produces codes that silently fail to scan with
no useful error for the player.

Adds a three-card first-run walkthrough, an explanation of what a
multiplier target means, and a one-time confirmation before a player's
first real-money action — the interface is deliberately frictionless, and
that is the one place a moment of friction is worth it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-08-07 01:05:52 +00:00
parent 2eafcbfd68
commit d7fa097eab
10 changed files with 1296 additions and 17 deletions

View File

@@ -585,3 +585,56 @@ button:active { transform: translateY(1px); }
#dep-amt, #wd-amt { margin: 0; }
.stakerow input { flex: 1.4; }
/* ---------- plain-language help ---------- */
.explain {
font-size: 11.5px; color: var(--green-dim); margin: 6px 2px 10px;
line-height: 1.45;
}
/* ---------- first-run walkthrough ---------- */
.tour {
position: fixed; inset: 0; z-index: 60;
background: #000000e8; backdrop-filter: blur(3px);
display: grid; place-items: center; padding: 20px;
}
.tourcard {
max-width: 340px; width: 100%; text-align: center;
background: #00120c; border: 1px solid var(--green);
border-radius: 3px; padding: 24px 20px;
box-shadow: 0 0 40px #00ff9c22;
}
.tourstep {
font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase;
color: var(--green-dim); margin-bottom: 10px;
}
.tourcard h2 {
font-size: 16px; color: var(--green); text-transform: none;
letter-spacing: 0.02em; margin-bottom: 10px;
}
.tourcard h2::before { content: none; }
.tourcard p {
font-size: 13px; line-height: 1.6; color: var(--green-dim);
margin: 0 0 18px;
}
.tourdots { display: flex; gap: 6px; justify-content: center; margin-bottom: 16px; }
.tourdots span {
width: 6px; height: 6px; border-radius: 50%;
background: var(--green-ghost);
}
.tourdots span.on { background: var(--green); box-shadow: 0 0 8px var(--green); }
.tourskip {
width: 100%; margin-top: 8px; border: none; background: none;
color: var(--green-dim); font-size: 11px;
}
/* ---------- real-money confirmation ---------- */
.confirm .tourcard { border-color: var(--amber); }
.confirm h2 { color: var(--amber); }
.confirm .primary {
background: #2a1c00; color: var(--amber); border-color: var(--amber);
}