GamificationAntino Labs
Spin the Wheel Feature
A spin-to-win reward rendered as a web view inside NeoZAP's app. The backend owns the wheel configuration and the prize; my side was making it arrive without flicker and behave exactly as designed, which meant forking the roulette library and rewriting its internals.
01
The problem
The backend already owned the wheel: which segments existed, what each was worth, how many attempts a user had left. What it did not own was how any of that felt, and the design team had specified a wheel the off-the-shelf library could not draw. Rendered naively the page also showed an empty wheel before the configuration arrived, shifted as the rest of the data landed, and the wheel jittered and sat slightly off its segment when it stopped.
02
The approach
The library went from a dependency to source I owned: cloned from GitHub and reworked until it matched the spec, covering segment appearance, the spin timing and easing, where it came to rest against the pointer, and how it rendered at phone sizes. Around that, nothing paints until the configuration is in hand, each phase of a spin is a named state, and the layout reserves its space up front. The app opens the URL with an access token on the query string, forwarded with every request, and attempts reset on the first spin of a new day rather than through a scheduled job.
Overview
- 3
- spins per user, per day
- 4
- library behaviours rewritten in the fork
- 0
- scheduled jobs to reset attempts
03
Key decisions
Forking react-custom-roulette and rewriting its internals
over Working within what the published API exposed
The design covered segment styling, the easing and length of the spin, exactly where the wheel settled against the pointer, and how it held up at phone widths. None of that was reachable through props, and the alternative was shipping a wheel that was visibly not the one the design team drew. I cloned the source and changed the implementation instead, which kept the rotation maths I did not need to rewrite while making the parts the spec cared about mine to control.
Holding the render until the configuration arrived
over Mounting the wheel with defaults and filling it in
A wheel drawn with placeholder segments and then corrected reads as broken, and it is the first thing a user sees. Waiting for the configuration cost a moment on load and removed the flicker completely.
A named state per phase of the spin
over A pair of loading and result booleans
The spin has more phases than it looks: waiting for the result, animating, revealing, and the case where the request fails and the attempt should not be spent. Naming each one made those rules something the UI followed rather than something reconstructed from flag combinations.
The access token forwarded from the query string
over A separate login inside the web view
The user is already signed in to the app; asking again inside a web view would have been a visible seam. The app opens the URL with a token attached, and every request carries it through, so landing fresh on that page is already an authenticated session.
Resetting attempts on the first spin of a new day
over A scheduled job resetting every user at midnight
A cron job means a fleet-wide write every night and a window where a user near the boundary sees the wrong count. Calculating the reset when they actually spin keeps it correct per user, needs no scheduler, and does no work for users who never come back.
04
What I built
Wheel render from backend config
React · REST API
Segments, values and attempt counts come from the API, and the wheel is drawn once they are in hand rather than corrected afterwards.
Spin lifecycle
Redux Toolkit
Each phase of a spin is a named state, so a failed request leaves the attempt unspent instead of the UI guessing.
Forked roulette library
react-custom-roulette (cloned) · Animation
Source cloned and reworked for segment appearance, spin timing and easing, landing alignment against the pointer, and rendering at phone sizes. Removed the jitter at rest and the off-centre stop.
Reward reveal
React
The prize is shown straight after the wheel stops, with the state held so a re-render does not replay the animation.
Rewards list
Next.js · REST API
Past wins listed in the mobile view, with loading and empty handled rather than left blank.
Token pass-through auth
Next.js · REST API
The access token the app puts on the URL is forwarded with every request, so a fresh landing is an authenticated session.
Daily attempt reset
REST API
Worked out server-side on the first spin of a new day, so no scheduled job and no stale counts.
05
The outcome
A wheel that matches the design rather than approximating it, arrives fully formed, gives each user three spins a day, and stops exactly on the prize it was given. The reward is shown immediately after the spin, with a list of past wins behind it on mobile.
Stack
- Next.js
- React
- TypeScript
- react-custom-roulette
- Redux Toolkit
- react-hook-form
- REST API
- Node.js
- PostgreSQL
- Sequelize
- Animation


