@zencemarketing/spin-scratch-sdk
v0.1.0-alpha.2
Published
A fully dynamic, configurable Spin & Win wheel and Scratch Card SDK – Vanilla JS & React.
Readme
SpinWheel SDK + ScratchCard SDK
A dynamic, configurable Spin & Win wheel and Scratch Card SDK for Vanilla JS and React (with full TypeScript support).
Package name: @zencemarketing/spin-scratch-sdk
Who is this for?
If you are a client developer, you generally only need to:
- Install the package
- Render a container (
<div id="..." />) - Call
SpinWheel.init(...)orScratchCard.init(...)(Vanilla) or use the React wrappers
Everything else (HTML structure, styles, DOM events, animations) is handled internally by the SDK.
Requirements
- Browser environment (needs
windowanddocument) - Node.js >= 16 for building this SDK
If you use SSR frameworks (Next.js, Remix), initialize the widgets only on the client (e.g. inside useEffect).
Features
| Feature | Details |
|---|---|
| Dynamic segments | 2–N segments – just pass an array |
| Fully configurable | 40+ options for SpinWheel, 50+ for ScratchCard |
| Spin physics | Configurable duration, spin count, easing |
| Spin limit | Restrict number of spins per session |
| Win card overlay | Auto-generated coupon code, copy button, configurable redeem CTA |
| Confetti | Toggleable confetti with custom colors and count |
| Appearance | Customize ring, pointer, button, background, shadows, animations |
| Callbacks | onSpinStart, onSpinEnd, onRedeem, onSpinLimitReached |
| Runtime updates | updateOptions(), updateSegments(), updatePrize() |
| React support | Thin wrapper with ref for imperative control |
| Zero dependencies | CSS is auto-injected; fonts loaded from Google Fonts |
Installation
Install from npm (when published)
npm i @zencemarketing/spin-scratch-sdkUse locally (not published yet)
In your client app package.json:
{
"dependencies": {
"@zencemarketing/spin-scratch-sdk": "file:../path-to/ZenceGamification/sdk"
}
}Then:
# In the SDK folder
npm i
npm run build
# In the client folder
npm iNote: Always point file: to the SDK root folder (the one that contains package.json), not dist/.
TypeScript support
This SDK ships TypeScript declarations out of the box — no @types/ package needed.
Two entry points
| Entry | Import path | Use case |
|---|---|---|
| Main | @zencemarketing/spin-scratch-sdk | Vanilla JS/TS, factory React |
| React | @zencemarketing/spin-scratch-sdk/react | Pre-wired React components |
Recommended import for React + TypeScript
import {
SpinWheelReact,
ScratchCardReact,
type SpinWheelReactProps,
type ScratchCardReactProps,
type SpinWheelReactHandle,
type ScratchCardReactHandle,
type SegmentConfig,
type ScratchPrize,
} from '@zencemarketing/spin-scratch-sdk/react';Recommended import for Vanilla TypeScript
import {
SpinWheel,
ScratchCard,
type SpinWheelOptions,
type ScratchCardOptions,
type SegmentConfig,
type ScratchPrize,
} from '@zencemarketing/spin-scratch-sdk';Key type tips
- Segment shape: Required fields are
labelandcolor(NOTname) - React props: Use
SpinWheelReactProps/ScratchCardReactProps— these already omitcontainer(the SDK handles it). Do NOT useOmit<SpinWheelOptions, 'container'>manually. - Ref handles: Use
SpinWheelReactHandle/ScratchCardReactHandleforuseRefgeneric.
Tip: If you hover on SpinWheel.init and you only see options: SpinWheelOptions, that’s normal. To see all available fields:
- Use IntelliSense inside
SpinWheel.init({ ... })(Ctrl+Space) - Or Ctrl/Cmd-click
SpinWheelOptionsto open the full type
Quick Start (Vanilla JS)
Option A: Use the UMD bundle in plain HTML
When the package is published, you can use a CDN like unpkg:
<div id="my-wheel"></div>
<!-- Uses the "unpkg" entry from package.json -->
<script src="https://unpkg.com/@zencemarketing/spin-scratch-sdk"></script>
<script>
var SpinWheel = window.SpinWheelSDK.SpinWheel;
var wheel = SpinWheel.init({
container: '#my-wheel',
segments: [
{ label: 'Free Gift', color: '#55efc4', icon: '🎁', title: 'Free Gift', worth: '₹299' },
{ label: 'Surprise Reward', color: '#81ecec', icon: '✨', title: 'Surprise Reward', worth: '₹199' },
{ label: 'Better Luck', color: '#dfe6e9', icon: '🌟', title: 'Better Luck', worth: '' },
],
headerTitle: 'Spin & Win',
headerSubtitle: 'Try your luck!',
onSpinEnd: function (prize, index) {
console.log('Won:', prize, 'at index', index);
},
});
// Optional: wheel.spin();
</script>Option B: Import in a bundler app (Vite/Webpack/etc.)
import { SpinWheel } from '@zencemarketing/spin-scratch-sdk';
const wheel = SpinWheel.init({
container: '#my-wheel',
segments: [
{ label: 'Prize 1', color: '#55efc4' },
{ label: 'Prize 2', color: '#81ecec' },
],
});Quick Start (React)
There are two ways to use this SDK in React.
Option A (recommended): Import from /react subpath
The simplest way — components are pre-wired, fully typed, and just work:
import React, { useRef } from 'react';
import {
SpinWheelReact,
type SpinWheelReactHandle,
type SegmentConfig,
} from '@zencemarketing/spin-scratch-sdk/react';
export default function App() {
const ref = useRef<SpinWheelReactHandle>(null);
const segments: SegmentConfig[] = [
{ label: 'Free Gift', color: '#55efc4', icon: '🎁', title: 'Free Gift', worth: '₹299' },
{ label: 'Surprise', color: '#81ecec', icon: '✨', title: 'Surprise', worth: '₹199' },
{ label: 'Better Luck', color: '#dfe6e9', icon: '🌟' },
];
return (
<SpinWheelReact
ref={ref}
headerTitle="Spin & Win"
segments={segments}
onSpinEnd={(prize: SegmentConfig, idx: number) => console.log('Won:', prize, idx)}
/>
);
}Option B: Factory function (advanced)
If you need more control or want to avoid the /react subpath:
import React, { useRef } from 'react';
import { createSpinWheelReact, type SpinWheelReactHandle } from '@zencemarketing/spin-scratch-sdk';
const SpinWheelReact = createSpinWheelReact(React);
export default function App() {
const ref = useRef<SpinWheelReactHandle>(null);
return (
<SpinWheelReact
ref={ref}
headerTitle="Spin & Win"
segments={[
{ label: 'Free Gift', color: '#55efc4', icon: '🎁', title: 'Free Gift', worth: '₹299' },
{ label: 'Surprise', color: '#81ecec', icon: '✨', title: 'Surprise', worth: '₹199' },
]}
onSpinEnd={(prize, idx) => console.log('Won:', prize, idx)}
/>
);
}Option B: Use Vanilla API inside useEffect
This gives you full control, but you must clean up on unmount.
import React, { useEffect } from 'react';
import { SpinWheel } from '@zencemarketing/spin-scratch-sdk';
export default function App() {
useEffect(() => {
const wheel = SpinWheel.init({
container: '#spin-wheel-container',
segments: [
{ label: 'Prize 1', color: '#55efc4' },
{ label: 'Prize 2', color: '#81ecec' },
],
});
return () => {
wheel.destroy();
};
}, []);
return <div id="spin-wheel-container" />;
}Tip (React StrictMode): React may mount/unmount twice in development. Always keep the cleanup (destroy()) to avoid duplicate widgets.
ScratchCard quick start (Vanilla)
import { ScratchCard } from '@zencemarketing/spin-scratch-sdk';
const card = ScratchCard.init({
container: '#scratch-card-container',
prize: { name: 'Gift Voucher', icon: '🎁', label: 'Congratulations!' },
onReveal: (prize) => console.log('Revealed:', prize),
});ScratchCard quick start (React)
import React, { useRef } from 'react';
import {
ScratchCardReact,
type ScratchCardReactHandle,
type ScratchPrize,
} from '@zencemarketing/spin-scratch-sdk/react';
export default function App() {
const ref = useRef<ScratchCardReactHandle>(null);
return (
<ScratchCardReact
ref={ref}
prize={{ name: 'Gift Voucher', icon: '🎁', label: 'Congratulations!' }}
onReveal={(prize: ScratchPrize) => console.log('Revealed:', prize)}
/>
);
}Or using the factory pattern:
import React, { useRef } from 'react';
import { createScratchCardReact, type ScratchCardReactHandle } from '@zencemarketing/spin-scratch-sdk';
const ScratchCardReact = createScratchCardReact(React);
export default function App() {
const ref = useRef<ScratchCardReactHandle>(null);
return (
<ScratchCardReact
ref={ref}
prize={{ name: 'Gift Voucher', icon: '🎁', label: 'Congratulations!' }}
onReveal={(prize) => console.log('Revealed:', prize)}
/>
);
}SpinWheel API Reference
SpinWheel.init(options) → SpinWheelInstance
Core Options
| Option | Type | Default | Description |
|---|---|---|---|
| container | string \| HTMLElement | required | CSS selector or DOM element to mount into |
| segments | Array<Segment> | required | Array of prize segments (min 2) |
| winningIndex | number \| null | null | Pre-determined winning index. Overridden by forceIndex in .spin() |
Spin Behavior
| Option | Type | Default | Description |
|---|---|---|---|
| spinDuration | number | 5000 | Total spin animation duration (ms) |
| minSpins | number | 5 | Minimum full rotations |
| maxSpins | number | 8 | Maximum full rotations |
| spinLimit | number \| null | null | Max spins allowed (null = unlimited) |
Header UI
| Option | Type | Default | Description |
|---|---|---|---|
| headerTitle | string \| null | null | Title above the wheel |
| headerSubtitle | string \| null | null | Subtitle below the title |
| titleColor | string \| null | null | Custom title color (uses theme.goldLight if null) |
| subtitleColor | string \| null | null | Custom subtitle color (uses theme.textMuted if null) |
Hub / Center
| Option | Type | Default | Description |
|---|---|---|---|
| hubLabel | string | 'Spin & win' | Text inside the center hub |
| hubIcon | string | '▲' | Icon inside the center hub |
Spin Button
| Option | Type | Default | Description |
|---|---|---|---|
| showButton | boolean | true | Show the external SPIN! button |
| buttonText | string | 'SPIN!' | Button label text |
Wheel Appearance
| Option | Type | Default | Description |
|---|---|---|---|
| backgroundColor | string \| null | null | Container background color |
| backgroundImage | string \| null | null | Background image URL |
| ringColor | string \| null | null | Wheel ring color |
| ringShadow | boolean | true | Enable ring shadow/glow |
| ringAnimation | boolean | true | Enable pulsing ring animation |
| pointerColor | string \| null | null | Pointer/arrow color |
| buttonColor | string \| null | null | Spin button gradient color |
| buttonShadow | boolean | true | Enable button shadow |
| buttonAnimation | boolean | true | Enable button hover/active animations |
Win Card
| Option | Type | Default | Description |
|---|---|---|---|
| showWinCard | boolean | true | Show win card overlay after spin |
| generateCode | boolean | true | Auto-generate a random coupon code |
| codeLength | number | 9 | Length of auto-generated codes |
| redeemUrl | string \| null | null | URL for redeem button |
| winCardBrandLabel | string \| null | null | Brand label on win card (uses hubLabel if null) |
| winCardWorthLabel | string | 'WORTH' | Label before worth value |
| winCardRedeemButtonText | string | 'Redeem Now' | Redeem button text |
| winCardRedeemButtonColorTop | string | '#15803d' | Redeem button gradient top color |
| winCardRedeemButtonColorBottom | string | '#166534' | Redeem button gradient bottom color |
| winCardRedeemButtonTextColor | string | '#ffffff' | Redeem button text color |
Confetti
| Option | Type | Default | Description |
|---|---|---|---|
| confettiOnWin | boolean | true | Show confetti on win |
| confettiColors | string[] \| null | null | Array of confetti colors |
| confettiCount | number | 40 | Number of confetti pieces |
Theme
| Option | Type | Default | Description |
|---|---|---|---|
| theme | object | See below | Color theme overrides |
Callbacks
| Option | Type | Default | Description |
|---|---|---|---|
| onSpinStart | function | null | (winIndex) => void |
| onSpinEnd | function | null | (prize, winIndex) => void |
| onRedeem | function | null | (prize) => void |
| onSpinLimitReached | function | null | (count) => void |
Segment Object
{
label: 'Free Gift', // text on the wheel (required)
color: '#55efc4', // segment background color (required)
icon: '🎁', // emoji/icon (optional)
title: 'Free Gift Voucher', // title on win card (optional, falls back to label)
worth: '₹299', // "WORTH ₹299" on win card (optional)
code: 'FIXEDCODE', // fixed code – overrides auto-generate (optional)
}SpinWheel Instance Methods
| Method | Description |
|---|---|
| .spin(forceIndex?) | Trigger a spin; optionally force which segment wins |
| .updateSegments(segments[]) | Replace all segments at runtime and re-render |
| .hideWinCard() | Programmatically close the win card overlay |
| .updateOptions(opts) | Update configuration options at runtime |
| .resetSpinCount(count?) | Reset the spin count (default: 0) |
| .destroy() | Remove all SDK-created DOM and clean up |
SpinWheel Instance Getters
| Getter | Type | Description |
|---|---|---|
| .lastWonPrize | Segment \| null | The last prize that was won |
| .lastWonIndex | number | The last won segment index (-1 if none) |
| .spinCount | number | Total spins performed |
| .remainingSpins | number \| null | Remaining spins (null if unlimited) |
SpinWheel Theme Defaults
{
gold: '#e8c547',
goldLight: '#f5d76e',
goldDark: '#c9a227',
bgDark: '#0d0d12',
textMuted: '#9ca3af',
}ScratchCard API Reference
ScratchCard.init(options) → ScratchCardInstance
Core Options
| Option | Type | Default | Description |
|---|---|---|---|
| container | string \| HTMLElement | required | CSS selector or DOM element |
| prize | Prize | required | Prize object: { name, icon, label } |
Header UI
| Option | Type | Default | Description |
|---|---|---|---|
| headerTitle | string | 'Scratch the Card...' | Header text |
| headerTitleColor | string \| null | null | Header title color |
Instruction
| Option | Type | Default | Description |
|---|---|---|---|
| instruction | string | 'Scratch the card...' | Instruction text |
| instructionColor | string \| null | null | Instruction text color |
Scratch Behavior
| Option | Type | Default | Description |
|---|---|---|---|
| revealThreshold | number | 55 | % scratched to auto-reveal |
| brushSize | number | 28 | Scratch brush size in px |
Coin / Eraser Tool
| Option | Type | Default | Description |
|---|---|---|---|
| coinSize | number | 56 | Coin cursor size in px |
| showCoin | boolean | true | Show coin cursor |
| coinIcon | string | '$' | Icon on coin |
| coinGradientStart | string \| null | null | Coin gradient start color |
| coinGradientEnd | string \| null | null | Coin gradient end color |
Card Appearance
| Option | Type | Default | Description |
|---|---|---|---|
| cardBackground | string \| null | null | Card background CSS |
| cardShadow | boolean | true | Enable card shadow |
| cardBorderRadius | number | 24 | Card border radius in px |
Scratch Zone
| Option | Type | Default | Description |
|---|---|---|---|
| scratchZoneBackground | string \| null | null | Scratch zone background |
| scratchZoneShadow | boolean | true | Enable zone shadow |
| scratchZoneBorderRadius | number | 20 | Zone border radius in px |
Scratch Layer
| Option | Type | Default | Description |
|---|---|---|---|
| scratchLayerColor | string | 'rgb(150,130,180)' | Scratch layer color |
| scratchLayerSparkles | boolean | true | Show sparkles on layer |
| scratchLayerSparkleCount | number | 40 | Number of sparkles |
Prize Display
| Option | Type | Default | Description |
|---|---|---|---|
| prizeTextColor | string \| null | null | Prize label color |
| prizeNameColor | string \| null | null | Prize name color |
| prizeIconBackground | string \| null | null | Prize icon background |
Gift Icon Hint
| Option | Type | Default | Description |
|---|---|---|---|
| showGiftIcon | boolean | true | Show gift icon hint |
| giftIcon | string | '🎁' | Gift icon emoji |
| giftIconBackground | string \| null | null | Gift icon background |
Modal
| Option | Type | Default | Description |
|---|---|---|---|
| showModal | boolean | true | Show modal after reveal |
| modalTitle | string | 'Congratulations!' | Modal title |
| modalTitleColor | string \| null | null | Modal title color |
| modalButtonText | string | 'Claim your' | Modal button text prefix |
| modalButtonColor | string \| null | null | Modal button background |
| modalButtonTextColor | string \| null | null | Modal button text color |
| modalBackdropBlur | boolean | true | Enable backdrop blur |
Confetti
| Option | Type | Default | Description |
|---|---|---|---|
| confettiEnabled | boolean | true | Enable confetti |
| confettiColors | string[] \| null | null | Confetti colors array |
| confettiCount | number | 100 | Number of confetti pieces |
| confettiDuration | number | 5500 | Confetti duration in ms |
Animation
| Option | Type | Default | Description |
|---|---|---|---|
| animationType | string | 'default' | 'default' | 'bounce' | 'none' |
| animationDuration | number | 600 | Animation duration in ms |
Theme
| Option | Type | Default | Description |
|---|---|---|---|
| theme | object | See below | Color theme overrides |
Callbacks
| Option | Type | Default | Description |
|---|---|---|---|
| onScratchStart | function | null | () => void |
| onScratchProgress | function | null | (percent) => void |
| onReveal | function | null | (prize) => void |
| onClaim | function | null | (prize) => void |
Prize Object
{
name: 'iPhone 16', // Prize name (required)
icon: '📱', // Emoji/icon (optional)
label: 'Congratulations!', // Label above prize name (optional)
}ScratchCard Instance Methods
| Method | Description |
|---|---|
| .reveal() | Programmatically reveal the prize |
| .reset() | Reset the card for replay |
| .hideModal() | Hide the win modal |
| .showModal() | Show the win modal |
| .updatePrize(prize) | Update prize at runtime and reset card |
| .updateOptions(opts) | Update configuration at runtime |
| .destroy() | Remove all SDK-created DOM and clean up |
ScratchCard Instance Getters
| Getter | Type | Description |
|---|---|---|
| .scratchPercent | number | Current scratch progress (0-100) |
| .hasRevealed | boolean | Whether the prize has been revealed |
| .prize | Prize \| null | Current prize object |
ScratchCard Theme Defaults
{
purpleDark: '#4a2c6a',
purpleMid: '#6b4a8a',
purpleLight: '#8b6baa',
gold: '#d4a84b',
goldLight: '#e8c547',
goldDark: '#b8923a',
white: '#ffffff',
textDark: '#2d2d2d',
textMuted: '#6b6b6b',
}React Ref Methods
SpinWheelReact
| Method / Getter | Description |
|---|---|
| ref.current.spin(forceIndex?) | Trigger a spin |
| ref.current.updateSegments(segs) | Replace segments |
| ref.current.hideWinCard() | Close win card |
| ref.current.updateOptions(opts) | Update config |
| ref.current.resetSpinCount(count?) | Reset spin count |
| ref.current.spinCount | Current spin count |
| ref.current.remainingSpins | Remaining spins |
| ref.current.lastWonPrize | Last won prize |
| ref.current.lastWonIndex | Last won index |
| ref.current.instance | Raw SpinWheelInstance |
ScratchCardReact
| Method / Getter | Description |
|---|---|
| ref.current.reveal() | Reveal prize |
| ref.current.reset() | Reset card |
| ref.current.hideModal() | Hide modal |
| ref.current.showModal() | Show modal |
| ref.current.updatePrize(prize) | Update prize |
| ref.current.updateOptions(opts) | Update config |
| ref.current.scratchPercent | Scratch progress |
| ref.current.hasRevealed | Is revealed? |
| ref.current.prize | Current prize |
| ref.current.instance | Raw ScratchCardInstance |
File Structure
sdk/
├── src/
│ ├── SpinWheel.js ← SpinWheel Core SDK (vanilla JS)
│ ├── SpinWheelReact.js ← SpinWheel React wrapper (factory)
│ ├── ScratchCard.js ← ScratchCard Core SDK (vanilla JS)
│ ├── ScratchCardReact.js ← ScratchCard React wrapper (factory)
│ ├── styles.js ← SpinWheel styles
│ ├── scratchStyles.js ← ScratchCard styles
│ ├── index.js ← Main barrel export
│ ├── react.js ← React subpath entry (pre-wires factories)
│ └── utils.js ← Shared utilities
├── dist/ ← Built SDK files
│ ├── spin-wheel-sdk.umd.js ← UMD (browser <script>)
│ ├── spin-wheel-sdk.umd.min.js ← UMD minified
│ ├── spin-wheel-sdk.esm.mjs ← ESM (import/export)
│ ├── spin-wheel-sdk.cjs.js ← CommonJS (require)
│ ├── spin-wheel-sdk.d.ts ← TypeScript declarations (main)
│ ├── react.esm.mjs ← React subpath ESM
│ ├── react.cjs.js ← React subpath CJS
│ └── react.d.ts ← TypeScript declarations (React)
├── types/ ← Source TypeScript declarations (copied into dist/)
│ ├── spin-wheel-sdk.d.ts
│ └── react.d.ts
├── scripts/ ← Build helpers
│ └── copy-types.mjs
├── demo-vanilla-combined.html ← Combined Vanilla JS demo
├── demo-react-combined.html ← Combined React demo
├── package.json
├── CHANGELOG.md
├── LICENSE
└── README.mdLicense
MIT
