@pixels-online/pixels-client-js-sdk
v3.21.0
Published
Pixels Client JS SDK
Readme
Pixels BuildOn Client JS SDK
A powerful TypeScript SDK for integrating Pixels BuildOn offerwall functionality into web applications and games. This SDK provides real-time offer management, player progression tracking, and reward handling capabilities.
🚀 Features
- Real-time Offer Management - Live updates via Server-Sent Events (SSE)
- Player Progression Tracking - Monitor player stats, achievements, and conditions
- Reward System Integration - Handle various reward types (coins, items, exp, etc.)
- Event-Driven Architecture - React to offer events and player actions
- TypeScript Support - Full type safety and IntelliSense support
- Flexible Configuration - Customizable asset resolution and hooks
- Multi-Environment Support - Local, Test, staging, and production environments
- Auto-Reconnection - Robust connection management with retry logic
📦 Installation
npm install @pixels-online/pixels-client-js-sdk🔧 Quick Start
1. Basic Setup
import { OfferwallClient } from '@pixels-online/pixels-client-js-sdk';
const client = new OfferwallClient({
env: 'test', // or 'live' for production
tokenProvider: async () => {
// Fetch JWT token from your server. We recommend using our server-side SDK on your server for JWT creation
const response = await fetch('/api/auth/pixels-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const data = await response.json();
return data.token;
},
assetResolver: (_, id: string) => {
return {
name: exampleGameLib[id]?.name || 'Unknown2',
image: exampleGameLib[id]?.image,
};
},
fallbackRewardImage: 'https://example.com/default-reward.png',
hooks: {
onOfferSurfaced: (offer) => {
var random = Math.random() < 0.5;
return random; // 50% chance to show the offer
},
},
autoConnect: true,
});2. Initialize and Connect
// Initialize the client
await client.initialize();
// Listen for offers
client.on('offersUpdated', (offers) => {
console.log('Available offers:', offers);
displayOffersInUI(offers);
});
// Listen for player updates
client.on('playerUpdated', (player) => {
console.log('Player snapshot updated:', player.snapshot);
console.log('Player data updated:', player.data);
updatePlayerStatsUI(player);
});3. Render Offer UI/UX
Each offer object returned by the SDK includes helper methods for rendering offer state in your UI.
Checking if an Offer is Claimable
Use offer.canClaim() to determine whether the player has met all requirements and can claim the offer:
const claimable = offer.canClaim?.();
renderClaimButton(offer, { disabled: !claimable });Displaying Offer Progress
Use offer.getProgressPercent() to get the player's overall completion percentage (0–100):
const progress = offer.getProgressPercent?.() ?? 0;
// Example: render a progress bar
renderProgressBar(offer, { percent: progress });Rendering Completion Conditions
Use offer.getCompletionConditions() to get a list of individual conditions, each with isMet (boolean) and text (human-readable description):
const conditions = offer.getCompletionConditions?.() ?? [];
for (const condition of conditions) {
renderCondition({
text: condition.text,
completed: condition.isMet,
});
}
// Example React rendering
function OfferConditions({ offer }) {
const conditions = offer.getCompletionConditions?.() ?? [];
return (
<ul>
{conditions.map((c, i) => (
<li key={i} style={{ color: c.isMet ? 'green' : 'gray' }}>
{c.isMet ? '✓' : '○'} {c.text}
</li>
))}
</ul>
);
}Rendering Team Offer Sibling Progress
For team offers (offers where all team members must complete for anyone to claim), use offer.getSiblingProgress() to display each sibling's progress:
const isTeamOffer = offer.labels?.includes('team_offer');
if (isTeamOffer) {
const siblingProgress = offer.getSiblingProgress?.() ?? [];
for (const sibling of siblingProgress) {
renderSiblingRow({
playerId: sibling.playerId,
percentCompleted: sibling.percentCompleted,
isComplete: sibling.isComplete,
});
}
}
// Example React rendering
function TeamProgress({ offer }) {
const siblings = offer.getSiblingProgress?.() ?? [];
if (!siblings.length) return null;
return (
<div>
<h4>Team Progress</h4>
{siblings.map((s) => (
<div key={s.playerId}>
<span>{s.playerId}</span>
<span>{s.percentCompleted}%</span>
<span>{s.isComplete ? '✓ Complete' : 'In Progress'}</span>
</div>
))}
</div>
);
}3. Claim Offers
// Claim an offer
try {
const result = await client.claimOffer(offerId);
console.log('Offer claimed successfully:', result);
} catch (error) {
console.error('Failed to claim offer:', error);
}🛒 Shop Purchases
Let players buy your game's shop items without leaving your app. Requires
in-app shop purchases to be enabled for your org by Stacked
(allowInAppShopPurchases) — contact Stacked to get it turned on.
Two things to know up front:
- Game-scoped balance. Purchases spend the player's Stacked balance
earned in your game only — balance from other games can never be spent
through the client SDK. The player's in-game balance is already available
via
getStacked()(stacked.currencies). - Linked account required. The player must have linked their Stacked
account (
no-stacked-accountotherwise — send them through the stacked-link flow you already use).
Rendering the shop
getShopItems() returns everything a shop screen needs — only live,
in-window items are returned, and every price/limit shown here is re-checked
server-side at purchase time:
const items = await client.getShopItems();
const balance = client.getStacked()?.currencies?.['cur_your_currency']?.balance ?? 0;
for (const item of items) {
const price = item.prices?.['cur_your_currency']; // may be USD-converted server-side
renderShopEntry({
name: item.name, // + item.description, item.image, item.category
price,
soldOut: item.supply != null && (item.sold ?? 0) >= item.supply,
limitReached: item.maxPerPlayer != null && item.purchaseCount >= item.maxPerPlayer,
canAfford: price != null && balance >= price,
endsAt: item.availableUntil, // show a countdown if you like
});
}Buying with balance
// The price is resolved server-side from your catalog — the client never
// sends an amount.
import { OfferwallApiError } from '@pixels-online/pixels-client-js-sdk';
try {
const { purchaseId, rewards, spent } = await client.purchaseShopItem(
item._id,
'cur_your_currency',
);
// Show pending/success UI. The item itself is delivered to YOUR BACKEND
// via the `user.gain_rewards` webhook (source: 'shop') — grant it there,
// deduped on the webhook's `instanceId` (=== purchaseId).
} catch (error) {
if (error instanceof OfferwallApiError) {
switch (error.code) {
case 'insufficient-balance': // balance > 0 but below the price
case 'no-coin-balance': // zero/no balance in the spend currency
case 'no-player-data': // player has never earned in this game
// all three mean "not enough in-game balance" — send them to earn more
break;
case 'sold-out': // supply exhausted
case 'max-per-player-reached': // per-player cap hit
case 'no-stacked-account': // player hasn't linked Stacked
case 'purchase-in-progress': // a purchase is already in flight
case 'currency-not-priced': // item has no price for that currencyId
case 'in-app-shop-purchases-not-enabled': // org flag not granted
// handle each case in your UI
break;
}
}
}⚠️ Never blind-retry
purchaseShopItemafter a network failure. A timed-out request may still have succeeded server-side. Reconcile via your webhook (instanceId) orgetShopItems()purchaseCountinstead.
Delivery is the same webhook channel used for offer claims: your backend
receives user.gain_rewards with source: 'shop', campaignId (the shop
item id), instanceId (the purchase id — your idempotency key), and the
item's rewards.
The same catalog items can also be paid for with crypto — see the next
section. A common shop UI offers both: "Buy with balance" when
canAfford, and "Pay with crypto" as the alternative.
💳 Crypto Checkout (pay for shop items with crypto)
Players can also pay for a shop item on-the-fly with crypto — an ERC20
transfer from any wallet, no Stacked balance needed. The flow is: the server
quotes the catalog price and a deposit wallet → the player's wallet signs and
broadcasts transfer(operator, amount) → the SDK has the server verify the
mined transaction and execute delivery. Delivery lands on the same
user.gain_rewards webhook as every other shop purchase (dedupe on
instanceId) — your backend does not change at all.
Requirements: the same allowInAppShopPurchases org flag, a linked Stacked
account, and crypto exchange enabled by Stacked (deposit-disabled
otherwise). Any wallet may pay — nothing is credited to the payer's balance;
the funds buy the item for the signed-in player.
The SDK bundles no wallet libraries. Anything that satisfies the minimal
EIP-1193 interface (request({ method, params })) plugs in, and there are
three ways to integrate, from zero-effort to full control:
A. No wallet stack — let the SDK find a wallet
Discovery uses EIP-6963 announcements (Ronin, MetaMask, Coinbase, OKX…)
plus legacy injection points like window.ronin.provider.
// Optional: render your own wallet picker (EIP-6963 gives name + icon)
const wallets = await client.getAvailableWallets();
try {
const result = await client.purchaseShopItemWithCrypto(item._id, {
currencyId: 'cur_pixel',
network: 'ronin',
provider: wallets[0]?.provider, // omit to auto-pick the first found
onStatus: (status) => {
// 'connecting' | 'approving' | 'switching-chain' | 'awaiting-signature'
// | 'broadcast' | 'verifying' | 'executing' | 'delivered' | 'failed'
updateCheckoutUi(status);
},
});
// result: { success: true, status: 'approved', txHash, shopItemId }
} catch (error) {
if (error instanceof CryptoPurchaseError) {
// wallet-side, wallet-independent codes:
// 'no-wallet-available' | 'user-rejected' | 'wrong-chain' | 'wallet-error'
} else if (error instanceof OfferwallApiError) {
// server-side codes: 'sold-out', 'insufficient-balance' (wallet balance
// too low), 'shop-item-not-purchasable-with-crypto', 'deposit-disabled', …
}
}B. You already have a wallet stack (reown/wagmi/Tanto/Ronin SDK)
Give the SDK a lazy provider getter — resolved at purchase time, so it survives reconnects. Every major stack exposes an EIP-1193 provider:
const client = new OfferwallClient({
// ...
// reown AppKit: () => modal.getWalletProvider()
// wagmi connector: () => connector.getProvider()
// Ronin wallet SDK: () => walletSdk.getProvider()
// Tanto Connect: () => connector.getProvider()
getWalletProvider: () => modal.getWalletProvider(),
});
await client.purchaseShopItemWithCrypto(item._id, {
currencyId: 'cur_pixel',
network: 'ronin',
});C. Full control — you broadcast, the SDK does the rest
For AA/smart wallets, native bridges, or custom signing UX. The SDK's only wallet-facing need is a transaction hash:
// 1. Server-priced quote + prepared ERC20 transfer (no wallet involved)
const quote = await client.prepareCryptoPurchase(item._id, {
currencyId: 'cur_pixel',
network: 'ronin',
walletAddress: myAddress,
});
// quote.tx = { to: <token contract>, data: <transfer calldata>, value: '0x0' }
// quote.chainId = the chain the transfer MUST be broadcast on
// 2. Broadcast however you like — on quote.chainId!
const txHash = await mySmartWallet.sendTransaction({ ...quote.tx, chainId: quote.chainId });
// 3. Hand back the hash — the SDK verifies + executes delivery
const result = await client.submitCryptoPurchase(quote, txHash);Crash recovery — call this on startup
Funds leave the wallet at broadcast, but the server only learns about the
transaction at verify. The SDK persists every broadcast hash to
localStorage and can finish an interrupted purchase later:
// after auth, once per app start
client.resumePendingCryptoPurchases().catch(console.error);⚠️ Never re-broadcast a payment after a failure. A
broadcast-or-later failure means the money is on-chain; recovery is always by transaction hash (submitCryptoPurchase/resumePendingCryptoPurchases), which is replay-safe — delivery fires exactly once server-side.
Result semantics: success: true means the server accepted the funds and
dispatched delivery — treat your user.gain_rewards webhook (deduped on
instanceId) as the source of truth for the item being granted. A result of
{ success: false, status: 'delivery-failed' } means the payment was
accepted but the item could not be delivered (e.g. it sold out between quote
and finalize) — surface a "contact support" state with the txHash.
status: 'approving' is non-terminal: the purchase is still in flight and a
later resumePendingCryptoPurchases() will finish it. Pending records are
strictly scoped to the player who made them — another account on the same
browser can never resume (or receive) someone else's purchase.
Progress is also emitted as an event, mirroring onStatus:
client.events.on(OfferEvent.CRYPTO_PURCHASE_STATUS, ({ status, shopItemId, txHash }) => {
// drive a global checkout indicator
});🎁 Gift Cards (Bitrefill)
Let a player cash out the balance they earned in your game as a real gift card, emailed to them by Bitrefill.
Funding is scoped to your game. The card is paid for with the player's
balance in this game only — never the cross-game aggregate on their Stacked
account, which stays behind the Stacked app's own consent. Likewise,
getGiftCardHistory() returns only the redemptions your game funded.
Availability is double-gated: your org needs allowInAppShopPurchases (the
same grant the shop surface uses) and the platform-wide bitrefill feature
flag must be on. getGiftCardConfig() collapses both into one answer.
1. Decide whether to show the section
const config = await client.getGiftCardConfig();
if (!config) return; // gift cards are off platform-wide — render nothing
// config.currencies[].balance is the player's balance IN THIS GAME
const pixel = config.currencies.find((c) => c.id === 'cur_pixel');
if (!pixel || pixel.balance <= 0) return;getGiftCardConfig() rejects (rather than returning null) when the player
specifically can't redeem — in-app-shop-purchases-not-enabled,
no-stacked-account, account-restricted — so you find out before showing a
CTA that would fail.
2. Validate the amount before quoting
min / max / step constrain the net card value — what the player
receives after the surcharge — not the gross their balance converts to.
This is the single easiest thing to get wrong, so the SDK ships the same
arithmetic the server runs:
import {
giftCardValueIssue,
giftCardGrossForNet,
snapGiftCardValue,
} from '@pixels-online/pixels-client-js-sdk';
// "I want a $25 card" → is that purchasable?
const issue = giftCardValueIssue(config, 25);
// null | 'usd-amount-too-small' | 'usd-amount-too-large' | 'usd-amount-not-on-step'
// Snap a slider to the nearest valid amount (always rounds DOWN)
const valid = snapGiftCardValue(config, 27); // e.g. 20 on a $10-step product
// USD the player must spend to land on a $25 card
const grossUsd = giftCardGrossForNet(config, 25);3. Quote, then purchase
const quote = await client.quoteGiftCard({
sourceCurrencyId: 'cur_pixel',
amount: 5000, // in-game currency to spend
});
// quote.targetAmountAfterFee is the card value the player receives.
// quote.priceUsd is the gross; quote.totalFee is the surcharge between them.
if (!quote.canWithdraw) return; // withdrawal limits currently block this
const result = await client.purchaseGiftCard({
sourceCurrencyId: 'cur_pixel',
amount: 5000,
recipientEmail: '[email protected]', // REQUIRED — Bitrefill mails the code here
recipientName: 'Player One', // optional
});
// { pendingWithdrawId, status: 'waiting', targetAmountAfterFee, ... }quoteGiftCard() persists nothing and moves no balance, so it is safe to call
on every keystroke (debounced). Re-quote before purchasing if the player
idles — the FX rate behind exchangeRate moves and only the purchase is
authoritative.
purchaseGiftCard() is not idempotent — each call is a separate purchase.
Disable the CTA while one is in flight. A rejected redemption refunds the
balance automatically; a failure to create one never debits.
4. Show redemption status
The purchase returns immediately with status: 'waiting'; Bitrefill is
charged asynchronously. Poll history for the outcome:
const { payouts } = await client.getGiftCardHistory({ page: 1, limit: 25 });
for (const p of payouts) {
if (p.status === 'complete') {
// code/link are only present once complete
console.log(p.bitrefill?.code, p.bitrefill?.link);
} else if (p.status === 'rejected') {
console.log('refunded:', p.error);
} else {
// 'waiting' / 'approved' / 'processing' — p.autoApprovalEta is a hint
console.log('pending', p.bitrefill?.hasCode);
}
}The server withholds code and link until the payout is complete;
bitrefill.hasCode tells you a code exists without exposing it.
📊 Player Data Structure
The SDK provides player information through the IClientPlayer interface:
interface IClientPlayer {
snapshot: IPlayerSnapshot; // Core player data (levels, currencies, achievements, etc.)
data?: IPlayerData | null; // Additional game-specific data
}Accessing Player Information
// Get the current player
const player = client.getPlayer();
if (player) {
// Access core player data
console.log('Player ID:', player.snapshot.playerId);
console.log('Player level:', player.snapshot.levels?.combat?.level);
console.log('Currency balance:', player.snapshot.currencies?.gold?.balance);
// Access additional player data (if available)
if (player.data) {
console.log('Additional currencies:', player.data.currencies);
}
}🎯 Configuration Options
OfferwallConfig
interface OfferwallConfig {
/** Environment: 'test' | 'live' | custom endpoint */
env: 'test' | 'live' | (string & {});
/** Auto-connect on initialization (default: false) */
autoConnect?: boolean;
/** Enable auto-reconnection (default: true) */
reconnect?: boolean;
/** Reconnection delay in ms (default: 1000) */
reconnectDelay?: number;
/** Max reconnection attempts (default: 5) */
maxReconnectAttempts?: number;
/** Enable debug logging (default: false) */
debug?: boolean;
/** Event hooks for custom logic */
hooks?: Partial<OfferwallHooks>;
/** Custom asset resolver for rewards */
assetResolver?: AssetResolver;
/** Fallback image for unknown rewards */
fallbackRewardImage: string;
/** JWT token provider function */
tokenProvider: TokenProvider;
}🎨 Asset Resolution
Customize how rewards are displayed in your game:
const gameAssets = {
gems: { name: 'Gems', image: '/assets/gems.png' },
gold: { name: 'Gold Coins', image: '/assets/gold.png' },
sword_1: { name: 'Iron Sword', image: '/assets/sword_iron.png' },
};
const client = new OfferwallClient({
// ... other config
assetResolver: (reward, assetId) => {
const asset = gameAssets[assetId];
if (asset) {
return { name: asset.name, image: asset.image };
}
return { name: `Unknown Item (${assetId})`, image: null };
},
});🎣 Event Hooks
Implement custom logic with event hooks:
const client = new OfferwallClient({
// ... other config
hooks: {
// Control which offers to show
onOfferSurfaced: (offer) => {
// Custom logic to determine if offer should be shown
const currentPlayer = client.getPlayer();
return currentPlayer?.snapshot.levels?.overall?.level >= offer.minLevel;
},
// Handle successful offer claims
onOfferClaimed: async (offer, rewards) => {
console.log('Offer completed!', { offer, rewards });
// Award rewards in your game
await showConfetti(rewards);
},
// Handle connection events
onConnect: () => {
console.log('Connected to Pixels BuildOn');
showConnectionStatus('connected');
},
onDisconnect: () => {
console.log('Disconnected from Pixels BuildOn');
showConnectionStatus('disconnected');
},
},
});📡 Events
Listen to various events emitted by the client:
// Offer-related events
client.on('offersUpdated', (offers) => {
/* Handle offers update */
});
client.on('offerAdded', (offer) => {
/* Handle new offer */
});
client.on('offerRemoved', (offerId) => {
/* Handle offer removal */
});
client.on('offerUpdated', (offer) => {
/* Handle offer changes */
});
// Player-related events
client.on('playerUpdated', (player) => {
/* Handle player data changes */
});
// Connection events
client.on('connected', () => {
/* Handle connection */
});
client.on('disconnected', () => {
/* Handle disconnection */
});
client.on('reconnecting', (attempt) => {
/* Handle reconnection attempts */
});
// Error events
client.on('error', (error) => {
/* Handle errors */
});🏗️ API Reference
OfferwallClient Methods
initialize(): Promise<void>
Initialize the client and establish connection.
disconnect(): Promise<void>
Disconnect from the service.
refreshOffersAndPlayer(): { offers: IClientOffer[], player: IClientPlayer }
Refresh and get all current offers.
claimOffer(offerId: string): Promise<ClaimResult>
Claim an offer and receive rewards.
getConnectionState(): ConnectionState
Get current connection status.
Shop Methods
getShopItems(): Promise<ClientShopItem[]>
Live shop items for this game, including this player's purchaseCount per
item. Requires the org's allowInAppShopPurchases flag.
purchaseShopItem(shopItemId: string, currencyId: string): Promise<ShopPurchaseResult>
Buy an item with the player's Stacked balance earned in this game. Price is
catalog-resolved server-side; delivery via the user.gain_rewards webhook
(dedupe on instanceId === purchaseId). Never blind-retry on network
failure.
Crypto Checkout Methods
getAvailableWallets(): Promise<DiscoveredWallet[]>
Wallets installed on the page (EIP-6963 metadata + legacy injection points
like window.ronin.provider) — render a picker and pass a result's
provider to purchaseShopItemWithCrypto.
purchaseShopItemWithCrypto(shopItemId: string, opts: CryptoPurchaseOptions): Promise<CryptoPurchaseResult>
One-shot crypto checkout: connect → quote → chain-switch → sign → verify →
execute. Provider resolution: opts.provider → config.getWalletProvider()
→ discovery → rejects CryptoPurchaseError('no-wallet-available'). Progress
via opts.onStatus and the CRYPTO_PURCHASE_STATUS event.
prepareCryptoPurchase(shopItemId, { currencyId, network, walletAddress }): Promise<CryptoPurchaseQuote>
Wallet-free quote: server-resolved price + prepared ERC20 transfer request
(quote.tx, broadcast on quote.chainId). For integrators who broadcast
themselves (AA wallets, custom UX).
submitCryptoPurchase(purchase, txHash, opts?): Promise<CryptoPurchaseResult>
Finish a purchase from its broadcast tx hash (verify → execute, replay-safe, persists the hash for crash recovery). Never re-broadcast — always resume by hash.
resumePendingCryptoPurchases(opts?): Promise<Array<CryptoPurchaseResult | { txHash, error }>>
Finish purchases interrupted mid-flow. Call once on startup after auth. Strictly scoped to the current player's own records.
listPendingCryptoPurchases(): PendingCryptoPurchase[]
Read-only view of every stored pending record for this env — including ones
resumePendingCryptoPurchases skips (identity mismatch, or saved before a
JWT could be fetched). Each record points at funds that already left a
wallet; surface these in support tooling instead of letting the 7-day TTL
discard them invisibly.
Gift Card Methods
getGiftCardConfig(): Promise<GiftCardConfig | null>
Availability, Bitrefill's purchasable bounds, and the player's spendable
balances in this game, in one call. null = gift cards are off
platform-wide. Rejects with in-app-shop-purchases-not-enabled /
no-stacked-account / account-restricted when this specific player can't
redeem.
quoteGiftCard({ sourceCurrencyId, amount, recipientEmail? }): Promise<GiftCardQuote>
Server-priced, non-committal quote — persists nothing, moves no balance.
targetAmountAfterFee is the card value the player receives; check
canWithdraw before enabling the CTA.
purchaseGiftCard({ sourceCurrencyId, amount, recipientEmail, recipientName? }): Promise<GiftCardPurchaseResult>
Buy a card with this game's balance. recipientEmail is required — Bitrefill
mails the code there. Returns status: 'waiting'; not idempotent.
getGiftCardHistory(params?): Promise<PaginatedGiftCardPayouts>
Redemption status for payouts this game funded, newest first.
bitrefill.code/bitrefill.link appear only once status === 'complete'.
Utility Functions
import { meetsConditions, AssetHelper } from '@pixels-online/pixels-client-js-sdk';
// Gift card bounds math — mirrors the server's own arithmetic
import {
giftCardValueIssue, // null | 'usd-amount-too-small' | ... for a net card value
giftCardNetValue, // gross USD -> net card value after the surcharge
giftCardGrossForNet, // net card value -> gross USD needed (rounds up)
snapGiftCardValue, // snap DOWN to the nearest purchasable step
} from '@pixels-online/pixels-client-js-sdk';
// Check if player meets offer conditions
const canClaim = meetsConditions(player.snapshot, offer.surfacingConditions);
// Asset helper utilities
const assetHelper = new AssetHelper(assetResolver, fallbackImage);
const rewardAsset = assetHelper.resolveRewardAsset(reward, assetId);🌍 Environments
The SDK supports multiple environments:
test- Sandbox environment for developmentlive- Production environment
� Migration Guide
v1.0.0+ Breaking Changes
Player Data Structure Update
The player data structure has been updated to provide better separation between core player data and additional game-specific data:
- Old:
IClientPlayerSnapshot(flat structure) - New:
IClientPlayer(structured withsnapshotanddataproperties)
Migration Steps:
// Before (v0.x)
const player = client.getPlayer(); // IClientPlayerSnapshot
const level = player.levels?.combat?.level;
const gold = player.currencies?.gold?.balance;
// After (v1.0+)
const player = client.getPlayer(); // IClientPlayer
const level = player.snapshot.levels?.combat?.level;
const gold = player.snapshot.currencies?.gold?.balance;
// Access additional data (new feature)
const additionalCurrencies = player.data?.currencies;Event Handler Updates:
// Before
client.on('playerUpdated', (playerSnapshot) => {
updateUI(playerSnapshot.currencies);
});
// After
client.on('playerUpdated', (player) => {
updateUI(player.snapshot.currencies);
// Also handle additional data if needed
if (player.data) {
handleAdditionalData(player.data);
}
});�📋 Requirements
- Node.js 16+
- TypeScript 4.5+ (if using TypeScript)
- Modern browser with EventSource support
🤝 Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the AGPLv3 License - see the LICENSE.md file for details.
🔗 Links
📞 Support
For support and questions:
- Create an issue on GitLab
- Contact the Pixels team
Made with ❤️ by the Pixels team
