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 androotRefused by the imperative pull engine. - Local
PACKS_DATAarray (gem pack ids, display amounts, raw counts, prices). - Local
WARP_SVG_PATHconstant for the warp crystal icon. - Pack delay table
PACK_DELAYSfor deterministic stagger animations. - The
busyRefreentrancy 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(selectors => s.gems, plususeWalletStore.getState()for the current cost-check snapshot).bonusStore(selectors => s.bonusPoints, plusgetState()forgetPoints,getPendingCount,advanceBonus,shiftPendingReward,resetIfFull).data/pull-config:SINGLE_PULL_GEM_COST,TEN_PULL_GEM_COST,BANNERS.react-router-domuseNavigate(for account pill →/profile).
PUSHES TO
pullService.executePullPull({ bannerId, count, payment })— server-authoritative pull execution. Reads responsesuccess,newShips,v4Results(per-resultrarity,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-continuebutton,spawnBonusFlare(points),flashNode(tier),rarityToNum(rarity).bonusStore.advanceBonus(1)andbonusStore.shiftPendingReward()via theonBonusAdvance/shiftPendingBonuscallbacks handed to the engine.bonusStore.resetIfFull()from theonPullCompletecallback.- Navigates to
/profilefrom the account pill. - DOM mutations on
#display-warp(innerHTML refresh,currency-error/counter-pulseclass toggles),.spree-progress-syncfills, milestone.m-1….m-5node class swaps (passed,active,locked).
DOES NOT
- Does not validate gem cost server-side — it shakes the
#display-warpelement 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 viabuyGemPackserver RPC. - Does not directly mutate
walletStoreorbonusStoredata — all canonical state updates come from server responses or the engine callbacks. - Does not handle pull tickets —
useTicketsis hard-codedfalse, payment is always'gems'. - Does not render its own bottom tabs; uses the shared
BottomNavcomponent but replaces the Layout chrome (handled by Layout giving/shopfullscreen treatment). - Does not bind click handlers to individual pack cards — all clicks are routed through a single delegated
handleRootClicksodangerouslySetInnerHTMLreplacements 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 callsforceUpdateUI.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 againstbonusStore.- Delegated root click handler routes by data attribute:
data-pack-id→handleBuyPack,data-pull-count+data-banner-id→handlePull,#persona-continue→closeLegendary. - Two CSS animation classes used as transient signals:
currency-error(insufficient funds shake) andcounter-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/shoproute. useEffecton mount: initializes the pull engine with callbacks, returnsdestroyPullEngineon unmount.useEffectkeyed on[gems, bonusPoints]: callsforceUpdateUIto keep imperative DOM in sync with reactive store state.useEffectkeyed on[handlePull, handleBuyPack]: attaches the delegated root click handler.useEffecton mount: seeds milestone node classes from currentbonusPoints.handlePull(bannerId, count)andhandleBuyPack(packId)— bothuseCallback-wrapped async handlers guarded bybusyRef.
Pattern notes
- Hybrid React + imperative DOM: React owns the overall tree but
dangerouslySetInnerHTMLis used for the packs grid, and the pull engine mutates DOM directly. Cleanup is viadestroyPullEngineon unmount. - Event delegation on the root survives DOM replacement from
dangerouslySetInnerHTMLbecause it inspectsdata-*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
executePullPullresponse snapshot.pullServicesnapshotsoldXpandoldStarbefore mutating inventory so the engine can render the XP / star delta without re-reading any store. - Reentrancy guard
busyRefis auseRef<boolean>rather than state — it must update synchronously without triggering a re-render so rapid taps are dropped immediately. - Deterministic stagger delays:
PACK_DELAYSis a fixed array rather thanMath.random()to avoid DOM churn / animation jitter on re-renders. - Banner visual style alternates
solar/freebooterby index parity insideObject.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-continuebutton drivingcloseLegendary). - The
#display-starquery inforceUpdateUIis dead — it queries but never assigns to the result.