ShopScreen

PURPOSE

Fullscreen React shell for the S.I.G. Storefront & Pull Engine V32. Renders the top bar with wallet, a bonus reward progress bar, pull banners with ×1 / ×10 buttons, warp crystal gem packs, and the legendary deep-dive / pull-result overlays. Acts as the React framing for the imperative pull animation engine.

OWNS

  • The <div className="sig-shop"> root element and rootRef used by the imperative pull engine.
  • Local PACKS_DATA array (gem pack ids, display amounts, raw counts, prices).
  • Local WARP_SVG_PATH constant for the warp crystal icon.
  • Pack delay table PACK_DELAYS for deterministic stagger animations.
  • The busyRef reentrancy guard against rapid-fire pull / pack purchase taps.
  • DOM construction for: top bar, bonus reward bar, banners grid, gem packs grid, pull-result overlay, bonus chest intro overlay, legendary deep dive overlay.
  • Helper functions forceUpdateUI, syncBonusBars, updateBonusNodes, generateEdgesHtml, pullCostLabel.

READS FROM

  • walletStore (selector s => s.gems, plus useWalletStore.getState() for the current cost-check snapshot).
  • bonusStore (selector s => s.bonusPoints, plus getState() for getPoints, getPendingCount, advanceBonus, shiftPendingReward, resetIfFull).
  • data/pull-config: SINGLE_PULL_GEM_COST, TEN_PULL_GEM_COST, BANNERS.
  • react-router-dom useNavigate (for account pill → /profile).

PUSHES TO

  • pullService.executePullPull({ bannerId, count, payment }) — server-authoritative pull execution. Reads response success, newShips, v4Results (per-result rarity, shipId, unlocked, oldXp, newXp, oldStar, newStar).
  • shopService.buyGemPack(packId) — server RPC to credit gems.
  • pull-engine.ts: initPullEngine (on mount, with callbacks), destroyPullEngine (on unmount), startPull(cards) after a successful pull, closeLegendary() from the #persona-continue button, spawnBonusFlare(points), flashNode(tier), rarityToNum(rarity).
  • bonusStore.advanceBonus(1) and bonusStore.shiftPendingReward() via the onBonusAdvance / shiftPendingBonus callbacks handed to the engine.
  • bonusStore.resetIfFull() from the onPullComplete callback.
  • Navigates to /profile from the account pill.
  • DOM mutations on #display-warp (innerHTML refresh, currency-error / counter-pulse class toggles), .spree-progress-sync fills, milestone .m-1.m-5 node class swaps (passed, active, locked).

DOES NOT

  • Does not validate gem cost server-side — it shakes the #display-warp element on insufficient gems before calling the service.
  • Does not implement the visual pull sequence; that lives in pull-engine.ts.
  • Does not run a real payment flow — gem pack prices display as (infinity glyph) and the actual credit happens via buyGemPack server RPC.
  • Does not directly mutate walletStore or bonusStore data — all canonical state updates come from server responses or the engine callbacks.
  • Does not handle pull tickets — useTickets is hard-coded false, payment is always 'gems'.
  • Does not render its own bottom tabs; uses the shared BottomNav component but replaces the Layout chrome (handled by Layout giving /shop fullscreen treatment).
  • Does not bind click handlers to individual pack cards — all clicks are routed through a single delegated handleRootClick so dangerouslySetInnerHTML replacements keep working.

Signals

  • onCollectionTick — engine callback when each card is absorbed (currently a no-op stub for future inventory UI refresh).
  • onPullComplete — engine callback when the full sequence ends; resets bonus bar if full and calls forceUpdateUI.
  • onBonusAdvance — engine callback that advances bonus by 1, spawns a flare at the new point total, and flashes a milestone node if a tier was crossed.
  • getBonusPoints, shiftPendingBonus, getPendingBonusCount — pull-engine queries against bonusStore.
  • Delegated root click handler routes by data attribute: data-pack-idhandleBuyPack, data-pull-count + data-banner-idhandlePull, #persona-continuecloseLegendary.
  • Two CSS animation classes used as transient signals: currency-error (insufficient funds shake) and counter-pulse (wallet update pulse), each toggled via remove → force reflow (void display.offsetWidth) → add.

Entry points

  • Exported function component ShopScreen (named export). Mounted by the Layout at the /shop route.
  • useEffect on mount: initializes the pull engine with callbacks, returns destroyPullEngine on unmount.
  • useEffect keyed on [gems, bonusPoints]: calls forceUpdateUI to keep imperative DOM in sync with reactive store state.
  • useEffect keyed on [handlePull, handleBuyPack]: attaches the delegated root click handler.
  • useEffect on mount: seeds milestone node classes from current bonusPoints.
  • handlePull(bannerId, count) and handleBuyPack(packId) — both useCallback-wrapped async handlers guarded by busyRef.

Pattern notes

  • Hybrid React + imperative DOM: React owns the overall tree but dangerouslySetInnerHTML is used for the packs grid, and the pull engine mutates DOM directly. Cleanup is via destroyPullEngine on unmount.
  • Event delegation on the root survives DOM replacement from dangerouslySetInnerHTML because it inspects data-* attributes at click time rather than binding to specific elements.
  • Force-reflow trick: display.classList.remove(x); void display.offsetWidth; display.classList.add(x); restarts CSS animations.
  • Server-authoritative state model: client renders results locally for animation, but wallet / pity / inventory mutations all come from the executePullPull response snapshot. pullService snapshots oldXp and oldStar before mutating inventory so the engine can render the XP / star delta without re-reading any store.
  • Reentrancy guard busyRef is a useRef<boolean> rather than state — it must update synchronously without triggering a re-render so rapid taps are dropped immediately.
  • Deterministic stagger delays: PACK_DELAYS is a fixed array rather than Math.random() to avoid DOM churn / animation jitter on re-renders.
  • Banner visual style alternates solar / freebooter by index parity inside Object.values(BANNERS).map.
  • Three fullscreen overlays sit inside the same root: #pull-result-stage (grid of revealed cards), #bonus-chest-intro (chest animation), #pull-solo-stage (legendary deep dive with #persona-continue button driving closeLegendary).
  • The #display-star query in forceUpdateUI is dead — it queries but never assigns to the result.