Purchase UX: lock the crate, report in-page, deliver safely (#106) - #108
Conversation
Four problems with buying something, from the click to the item landing in an inventory. The crate modal could be dismissed mid-roll. The animation runs for as long as the crate's own setting says, and until it finishes the reward is only on the server; clicking the backdrop threw away the one screen that ever showed what was won. `crateLocked` is set when the roll starts and cleared a beat after it resolves; `closeCrate` returns early while it is set, and the OK button carries `.is-waiting` so the modal does not look frozen. Purchase results came out of `alert()`. Replaced with an in-page toast stack (`sfNotice`) that renders the product's own icon at item size, so a bought item looks like the thing that was bought rather than a browser dialog. Delivery was unsafe in both directions. The old rule ran the reward commands if the buyer's name was in the console's online list, else queued and fired 1500ms after the join line. On a server with a login plugin that is the worst moment to `give`: before `/login` the player is in a holding state where the item drops on the floor, and the plugin usually restores a saved inventory straight after, overwriting whatever arrived. Worse, `runCommands` returned void and did nothing when neither RCON nor stdin was up — while the caller had already dequeued the entry, so the reward simply vanished. `src/shared/delivery.ts` now decides from evidence: the server must be up and reachable, the player connected, and then either the bridge can see them in a world or `online-mode=true` makes the session trustworthy. A cracked server with no bridge holds for operator approval by default. Every branch either delivers or keeps the reward; a 32-combination sweep in the smoke asserts no input can drop one. `runCommands` returns a boolean and the queue entry is only removed once it has. Delivered rewards now announce themselves in game (tellraw, JSON-escaped), on the spot or on the join that releases them, so a player who bought something while offline finds out it arrived. New: GET /servers/:id/store/admin/pending and POST /servers/:id/store/admin/deliver for the held queue. Grace and the hold-when-unverified switch are config (`store.deliveryGraceMs`, `store.holdUnverifiedDeliveries`).
Two ways the queue still lost a paid-for reward. `attemptDelivery` persisted on hold and delivered on deliver, and on `wait` it did neither — the entry lived in a setTimeout closure and nowhere else, so quitting during the grace lost it. The 32-combination sweep could not see this: it tests the pure decision function, and the loss was in the caller. `queueReason()` now states the rule once — null only for a hand-over — and the caller persists whenever it returns anything. A waiting entry queues as `just-joined`, and a `scheduled` set stops a join mid-grace from delivering it a second time. The sweep asserts the rule and now sweeps joinedAgoMs, the axis the wait branch actually depends on. `runCommands` returned true whenever `isConnected` was true, ignoring that `rcon.tryCommand` answers null on failure. A connection dropping between the check and the send made the caller dequeue a reward that was never given — the exact bug this branch was written to close. Each command is now checked, and a failed RCON send falls back to stdin while the process is up.
Self-reviewTwo defects, both of them the same defect this PR exists to fix, reintroduced 1. The
|
Purchase UX: lock the crate, report in-page, deliver safely (#106)
Four problems with buying something, from the click to the item landing in
an inventory.
The crate modal could be dismissed mid-roll. The animation runs for as long
as the crate's own setting says, and until it finishes the reward is only on
the server; clicking the backdrop threw away the one screen that ever showed
what was won.
crateLockedis set when the roll starts and cleared a beatafter it resolves;
closeCratereturns early while it is set, and the OKbutton carries
.is-waitingso the modal does not look frozen.Purchase results came out of
alert(). Replaced with an in-page toast stack(
sfNotice) that renders the product's own icon at item size, so a boughtitem looks like the thing that was bought rather than a browser dialog.
Delivery was unsafe in both directions. The old rule ran the reward commands
if the buyer's name was in the console's online list, else queued and fired
1500ms after the join line. On a server with a login plugin that is the worst
moment to
give: before/loginthe player is in a holding state where theitem drops on the floor, and the plugin usually restores a saved inventory
straight after, overwriting whatever arrived. Worse,
runCommandsreturnedvoid and did nothing when neither RCON nor stdin was up — while the caller
had already dequeued the entry, so the reward simply vanished.
src/shared/delivery.tsnow decides from evidence: the server must be up andreachable, the player connected, and then either the bridge can see them in a
world or
online-mode=truemakes the session trustworthy. A cracked serverwith no bridge holds for operator approval by default. Every branch either
delivers or keeps the reward; a 32-combination sweep in the smoke asserts no
input can drop one.
runCommandsreturns a boolean and the queue entry isonly removed once it has.
Delivered rewards now announce themselves in game (tellraw, JSON-escaped),
on the spot or on the join that releases them, so a player who bought
something while offline finds out it arrived.
New: GET /servers/:id/store/admin/pending and POST /servers/:id/store/admin/deliver
for the held queue. Grace and the hold-when-unverified switch are config
(
store.deliveryGraceMs,store.holdUnverifiedDeliveries).