Events
Event system and listeners for the Client SDK
The Client SDK uses an event-driven architecture to notify your application about offers, player updates, connection changes, and errors. This page documents all available events and how to use them.
EventEmitter API
Access the event emitter through the events property:
const client = new OfferwallClient({ /* config */ });
// Listen to events
client.events.on('refresh', (data) => {
console.log('Offers refreshed:', data.offers);
});var client = new OfferwallClient(new OfferwallConfig { /* config */ });
// Listen to events
client.Events.On<RefreshEventData>("refresh", (data) =>
{
Console.WriteLine($"Offers refreshed: {data.Offers.Length}");
});Methods
on<T>() - Register Event Listener
Register a listener that will be called every time the event is emitted.
client.events.on('offer_surfaced', ({ offer }) => {
console.log('New offer:', offer.name);
showNotification(offer);
});client.Events.On<OfferSurfacedEventData>("offer_surfaced", (data) =>
{
Console.WriteLine($"New offer: {data.Offer.Name}");
ShowNotification(data.Offer);
});off<T>() - Remove Event Listener
Remove a previously registered event listener.
// Define handler
const handler = ({ offers }) => {
console.log('Offers:', offers);
};
// Register
client.events.on('refresh', handler);
// Unregister
client.events.off('refresh', handler);// Define handler
void HandleRefresh(RefreshEventData data)
{
Console.WriteLine($"Offers: {data.Offers.Length}");
}
// Register
client.Events.On<RefreshEventData>("refresh", HandleRefresh);
// Unregister
client.Events.Off<RefreshEventData>("refresh", HandleRefresh);once<T>() - One-Time Listener
Register a listener that will only be called once, then automatically removed.
client.events.once('connected', ({ timestamp }) => {
console.log('Connected! This will only log once.');
});client.Events.Once<ConnectedEventData>("connected", (data) =>
{
Console.WriteLine("Connected! This will only log once.");
});removeAllListeners() - Cleanup
Remove all listeners for a specific event, or all listeners for all events.
// Remove all listeners for 'refresh' event
client.events.removeAllListeners('refresh');
// Remove ALL listeners for ALL events
client.events.removeAllListeners();// Remove all listeners for 'refresh' event
client.Events.RemoveAllListeners("refresh");
// Remove ALL listeners for ALL events
client.Events.RemoveAllListeners();Connection Events
connected
Emitted when the client successfully connects to Stacked.
Payload:
{
timestamp: Date
}Example:
client.events.on('connected', ({ timestamp }) => {
console.log('Connected at:', timestamp);
showConnectionStatus('online');
enableOfferwallFeatures();
});client.Events.On<ConnectedEventData>("connected", (data) =>
{
Console.WriteLine($"Connected at: {data.Timestamp}");
ShowConnectionStatus("online");
EnableOfferwallFeatures();
});disconnected
Emitted when the connection is closed, either manually or due to an error.
Payload:
{
reason?: string; // 'manual' | 'max_reconnect_attempts' | etc.
timestamp: Date
}Example:
client.events.on('disconnected', ({ reason, timestamp }) => {
console.log(`Disconnected: ${reason}`);
showConnectionStatus('offline');
});client.Events.On<DisconnectedEventData>("disconnected", (data) =>
{
Console.WriteLine($"Disconnected: {data.Reason}");
ShowConnectionStatus("offline");
});connection_state_changed
Emitted whenever the connection state changes.
Payload:
{
state: ConnectionState; // Current state
previousState?: ConnectionState; // Previous state
error?: Error; // Error if state is 'error'
attempt?: number; // Current reconnection attempt
maxAttempts?: number; // Maximum reconnection attempts
}Connection States:
disconnected- Not connectedconnecting- Establishing connectionconnected- Active connectionreconnecting- Attempting to reconnecterror- Connection error occurred
Example:
client.events.on('connection_state_changed', (data) => {
console.log(`State: ${data.previousState} → ${data.state}`);
switch (data.state) {
case 'connecting':
showLoadingSpinner('Connecting...');
break;
case 'connected':
hideLoadingSpinner();
showSuccessMessage('Connected!');
break;
case 'reconnecting':
showLoadingSpinner(`Reconnecting...`);
break;
case 'error':
showErrorMessage(data.error?.message || 'Connection error');
break;
case 'disconnected':
showOfflineMessage();
break;
}
});client.Events.On<ConnectionStateChangedEventData>("connection_state_changed", (data) =>
{
Console.WriteLine($"State: {data.PreviousState} → {data.State}");
switch (data.State)
{
case ConnectionState.Connecting:
ShowLoadingSpinner("Connecting...");
break;
case ConnectionState.Connected:
HideLoadingSpinner();
ShowSuccessMessage("Connected!");
break;
case ConnectionState.Reconnecting:
ShowLoadingSpinner($"Reconnecting...");
break;
case ConnectionState.Error:
ShowErrorMessage(data.Error?.Message ?? "Connection error");
break;
case ConnectionState.Disconnected:
ShowOfflineMessage();
break;
}
});connection_error
Emitted when a connection error occurs.
Payload:
{
error: Error;
timestamp: Date
}Example:
client.events.on('connection_error', ({ error, timestamp }) => {
console.error('Connection error:', error);
// Log to error tracking
errorTracking.capture(error, {
context: 'connection',
timestamp,
});
});client.Events.On<ConnectionErrorEventData>("connection_error", (data) =>
{
Console.WriteLine($"Connection error: {data.Error.Message}");
// Log to error tracking
ErrorTracking.Capture(data.Error, new
{
Context = "connection",
Timestamp = data.Timestamp
});
});Offer Events
refresh
Emitted when offers and player data are refreshed. This includes:
- Initial connection (after
initialize()) - Manual refresh (calling
refreshOffersAndPlayer())
Payload:
{
offers: IClientOffer[];
player: IClientPlayer;
}Example:
client.events.on('refresh', ({ offers, player }) => {
console.log(`Loaded ${offers.length} offers`);
console.log('Player:', player.snapshot.playerId);
// Update UI with offers
displayOffers(offers);
updatePlayerStats(player.snapshot);
});client.Events.On<RefreshEventData>("refresh", (data) =>
{
Console.WriteLine($"Loaded {data.Offers.Length} offers");
Console.WriteLine($"Player: {data.Player.Snapshot.PlayerId}");
// Update UI with offers
DisplayOffers(data.Offers);
UpdatePlayerStats(data.Player.Snapshot);
});offer_surfaced
Emitted when a new offer surfaces to the player in real-time via the Stacked API.
Payload:
{
offer: IClientOffer
}Example:
client.events.on('offer_surfaced', ({ offer }) => {
console.log('New offer:', offer.name);
// Show notification to player
showOfferNotification({
title: offer.name,
description: offer.description,
image: offer.image,
rewards: offer.rewards,
});
// Play notification sound
playNotificationSound();
// Update offers list
refreshOffersList();
});client.Events.On<OfferSurfacedEventData>("offer_surfaced", (data) =>
{
Console.WriteLine($"New offer: {data.Offer.Name}");
// Show notification to player
ShowOfferNotification(new OfferNotification
{
Title = data.Offer.Name,
Description = data.Offer.Description,
Image = data.Offer.Image,
Rewards = data.Offer.Rewards
});
// Play notification sound
PlayNotificationSound();
// Update offers list
RefreshOffersList();
});Controlling Notifications
Use the onOfferSurfaced hook to control whether this event is emitted. Return true to emit the event, false to suppress it.
offer_claimed
Emitted when an offer is successfully claimed.
Payload:
{
instanceId: string
}Example:
client.events.on('offer_claimed', ({ instanceId }) => {
console.log('Offer claimed:', instanceId);
const offer = client.store.getOffer(instanceId);
if (offer) {
showSuccessMessage(`You claimed ${offer.rewards.length} rewards!`);
// Update UI to remove or mark as claimed
updateOfferStatus(instanceId, 'claimed');
}
});client.Events.On<OfferClaimedEventData>("offer_claimed", (data) =>
{
Console.WriteLine($"Offer claimed: {data.InstanceId}");
var offer = client.Store.GetOffer(data.InstanceId);
if (offer != null)
{
ShowSuccessMessage($"You claimed {offer.Rewards.Count} rewards!");
// Update UI to remove or mark as claimed
UpdateOfferStatus(data.InstanceId, "claimed");
}
});Error Events
error
Emitted when an error occurs within the SDK.
Payload:
{
error: Error;
context?: string; // Where the error occurred
}Common contexts:
connection- Connection-related errorsclaim- Offer claim errorssse_message_parse- Error parsing SSE messagerefresh- Error refreshing offers/player data
Example:
client.events.on('error', ({ error, context }) => {
console.error(`Error in ${context || 'unknown'}:`, error);
// Log to error tracking service
errorTracking.captureException(error, {
tags: { context },
});
// Show user-friendly error
if (context === 'claim') {
showErrorMessage('Failed to claim reward. Please try again.');
} else if (context === 'connection') {
// SDK will automatically retry
showInfoMessage('Connection issue. Retrying...');
}
});client.Events.On<ErrorEventData>("error", (data) =>
{
Console.WriteLine($"Error in {data.Context ?? "unknown"}: {data.Error.Message}");
// Log to error tracking service
ErrorTracking.CaptureException(data.Error, new
{
Tags = new { Context = data.Context }
});
// Show user-friendly error
if (data.Context == "claim")
{
ShowErrorMessage("Failed to claim reward. Please try again.");
}
else if (data.Context == "connection")
{
// SDK will automatically retry
ShowInfoMessage("Connection issue. Retrying...");
}
});Complete Event Reference
Prop
Type
Usage Patterns
React Hook
import { useEffect, useState } from 'react';
import { OfferwallClient, IClientOffer } from '@pixels-online/pixels-client-js-sdk';
function OfferwallComponent({ client }: { client: OfferwallClient }) {
const [offers, setOffers] = useState<IClientOffer[]>([]);
const [connectionState, setConnectionState] = useState('disconnected');
useEffect(() => {
// Handler functions
const handleRefresh = ({ offers }: { offers: IClientOffer[] }) => {
setOffers(offers);
};
const handleStateChange = ({ state }: { state: string }) => {
setConnectionState(state);
};
// Register listeners
client.events.on('refresh', handleRefresh);
client.events.on('connection_state_changed', handleStateChange);
// Cleanup on unmount
return () => {
client.events.off('refresh', handleRefresh);
client.events.off('connection_state_changed', handleStateChange);
};
}, [client]);
return (
<div>
<p>Status: {connectionState}</p>
<p>Offers: {offers.length}</p>
<ul>
{offers.map(offer => (
<li key={offer.instanceId}>{offer.name}</li>
))}
</ul>
</div>
);
}import { useEffect, useState } from 'react';
function OfferwallComponent({ client }) {
const [offers, setOffers] = useState([]);
const [connectionState, setConnectionState] = useState('disconnected');
useEffect(() => {
// Handler functions
const handleRefresh = ({ offers }) => {
setOffers(offers);
};
const handleStateChange = ({ state }) => {
setConnectionState(state);
};
// Register listeners
client.events.on('refresh', handleRefresh);
client.events.on('connection_state_changed', handleStateChange);
// Cleanup on unmount
return () => {
client.events.off('refresh', handleRefresh);
client.events.off('connection_state_changed', handleStateChange);
};
}, [client]);
return (
<div>
<p>Status: {connectionState}</p>
<p>Offers: {offers.length}</p>
<ul>
{offers.map(offer => (
<li key={offer.instanceId}>{offer.name}</li>
))}
</ul>
</div>
);
}Vue Composition API
<template>
<div>
<p>Status: {{ connectionState }}</p>
<p>Offers: {{ offers.length }}</p>
<ul>
<li v-for="offer in offers" :key="offer.instanceId">
{{ offer.name }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import type { OfferwallClient, IClientOffer } from '@pixels-online/pixels-client-js-sdk';
interface Props {
client: OfferwallClient;
}
const props = defineProps<Props>();
const offers = ref<IClientOffer[]>([]);
const connectionState = ref('disconnected');
const handleRefresh = ({ offers: newOffers }: { offers: IClientOffer[] }) => {
offers.value = newOffers;
};
const handleStateChange = ({ state }: { state: string }) => {
connectionState.value = state;
};
onMounted(() => {
props.client.events.on('refresh', handleRefresh);
props.client.events.on('connection_state_changed', handleStateChange);
});
onUnmounted(() => {
props.client.events.off('refresh', handleRefresh);
props.client.events.off('connection_state_changed', handleStateChange);
});
</script><template>
<div>
<p>Status: {{ connectionState }}</p>
<p>Offers: {{ offers.length }}</p>
<ul>
<li v-for="offer in offers" :key="offer.instanceId">
{{ offer.name }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps(['client']);
const offers = ref([]);
const connectionState = ref('disconnected');
const handleRefresh = ({ offers: newOffers }) => {
offers.value = newOffers;
};
const handleStateChange = ({ state }) => {
connectionState.value = state;
};
onMounted(() => {
props.client.events.on('refresh', handleRefresh);
props.client.events.on('connection_state_changed', handleStateChange);
});
onUnmounted(() => {
props.client.events.off('refresh', handleRefresh);
props.client.events.off('connection_state_changed', handleStateChange);
});
</script>Unity (C#)
using UnityEngine;
using PixelsOnline.Client.SDK;
using System.Collections.Generic;
public class OfferwallManager : MonoBehaviour
{
private OfferwallClient client;
private List<IClientOffer> offers = new List<IClientOffer>();
void Start()
{
// Initialize client
client = new OfferwallClient(new OfferwallConfig
{
Env = "test",
TokenProvider = GetTokenAsync,
FallbackRewardImage = "default-reward.png",
AutoConnect = true
});
// Register event listeners
client.Events.On<RefreshEventData>("refresh", HandleRefresh);
client.Events.On<ConnectionStateChangedEventData>("connection_state_changed", HandleStateChange);
client.Events.On<OfferSurfacedEventData>("offer_surfaced", HandleOfferSurfaced);
}
void OnDestroy()
{
// Clean up event listeners
client.Events.Off<RefreshEventData>("refresh", HandleRefresh);
client.Events.Off<ConnectionStateChangedEventData>("connection_state_changed", HandleStateChange);
client.Events.Off<OfferSurfacedEventData>("offer_surfaced", HandleOfferSurfaced);
// Disconnect client
client.DisconnectAsync().Wait();
}
private void HandleRefresh(RefreshEventData data)
{
offers = new List<IClientOffer>(data.Offers);
Debug.Log($"Loaded {offers.Count} offers");
// Update UI on main thread
UnityMainThreadDispatcher.Enqueue(() =>
{
UpdateOffersUI(offers);
});
}
private void HandleStateChange(ConnectionStateChangedEventData data)
{
Debug.Log($"Connection state: {data.PreviousState} → {data.State}");
UnityMainThreadDispatcher.Enqueue(() =>
{
UpdateConnectionStatus(data.State.ToString());
});
}
private void HandleOfferSurfaced(OfferSurfacedEventData data)
{
Debug.Log($"New offer: {data.Offer.Name}");
UnityMainThreadDispatcher.Enqueue(() =>
{
ShowOfferNotification(data.Offer);
});
}
private async System.Threading.Tasks.Task<string> GetTokenAsync()
{
// Fetch token from your server
var request = UnityWebRequest.Get("https://your-server.com/api/stacked-token");
await request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var response = JsonUtility.FromJson<TokenResponse>(request.downloadHandler.text);
return response.token;
}
throw new System.Exception("Failed to get token");
}
}Best Practices
Always Remove Listeners
// ❌ Bad - memory leak
component.mount(() => {
client.events.on('refresh', handleRefresh);
// Never removes the listener
});
// ✅ Good - proper cleanup
component.mount(() => {
client.events.on('refresh', handleRefresh);
});
component.unmount(() => {
client.events.off('refresh', handleRefresh);
});// ❌ Bad - memory leak
void Start()
{
client.Events.On<RefreshEventData>("refresh", HandleRefresh);
// Never removes the listener
}
// ✅ Good - proper cleanup
void Start()
{
client.Events.On<RefreshEventData>("refresh", HandleRefresh);
}
void OnDestroy()
{
client.Events.Off<RefreshEventData>("refresh", HandleRefresh);
}Use Named Functions for Removal
// ❌ Bad - can't remove anonymous function
client.events.on('refresh', ({ offers }) => {
console.log('Offers:', offers);
});
// ✅ Good - can remove named function
const handleRefresh = ({ offers }) => {
console.log('Offers:', offers);
};
client.events.on('refresh', handleRefresh);
// Later...
client.events.off('refresh', handleRefresh);// ❌ Bad - can't remove lambda
client.Events.On<RefreshEventData>("refresh", (data) =>
{
Console.WriteLine($"Offers: {data.Offers.Length}");
});
// ✅ Good - can remove named method
void HandleRefresh(RefreshEventData data)
{
Console.WriteLine($"Offers: {data.Offers.Length}");
}
client.Events.On<RefreshEventData>("refresh", HandleRefresh);
// Later...
client.Events.Off<RefreshEventData>("refresh", HandleRefresh);
Stacked