LogoStacked
Client SDK

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 connected
  • connecting - Establishing connection
  • connected - Active connection
  • reconnecting - Attempting to reconnect
  • error - 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 errors
  • claim - Offer claim errors
  • sse_message_parse - Error parsing SSE message
  • refresh - 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);