Skip to content

Events System

Overview

Balancy's event system provides hooks into the UI lifecycle, button interactions, and backend notifications. Use these events to coordinate initialization, respond to user actions, and react to backend changes.

Event Categories:

  • Lifecycle Events: Initialization stages
  • Component Events: Component system lifecycle (new!)
  • Button Events: User interactions
  • Backend Events: Server notifications
  • Custom Events: Custom messaging

Lifecycle Events

These events fire during view initialization, allowing you to coordinate async operations and delay the ready signal.

Event Flow

Page Load
    ↓
balancy-buttons-complete     ← All buttons processed
    ↓
balancy-localization-complete ← All text localized
    ↓
balancy-text-complete        ← All dynamic text updated
    ↓
balancy-images-complete      ← All images loaded
    ↓
balancy-fonts-complete       ← All fonts loaded
    ↓
balancy-components-complete  ← All script components initialized (NEW)
    ↓
balancy-audio-complete       ← All audio prepared
    ↓
balancy-ready                ← Everything ready, view shown

balancy-ready

Fired when all initialization is complete and the view is ready to be shown.

Usage:

document.addEventListener('balancy-ready', () => {
  console.log('Balancy UI is fully initialized');
  // Start animations, timers, etc.
});

// Or with once flag
window.addEventListener('balancy-ready', main, { once: true });

async function main() {
  // Your initialization code
  const offers = await balancy.getActiveOffers();
  displayOffers(offers);
}

Delaying Ready Signal:

If you need to load additional data before showing the view:

function main() {
  // Prevent automatic ready signal
  balancy.delayIsReady();

  // Load your data
  loadCustomData().then(() => {
    // Signal ready when done
    balancy.sendIsReady();
  });
}

window.addEventListener('balancy-ready', main, { once: true });

balancy-components-complete (New!)

Fired when all script components have been initialized (after awake() and onEnable() called).

Usage:

document.addEventListener('balancy-components-complete', () => {
  console.log('All components initialized');

  // Safe to query components
  const shopScript = elementsManager.getElementByGuid('shop-guid')
    .getComponent(Balancy_UI_LiveOps_Shop);

  shopScript.init(shopData);
});

balancy-images-complete

Fired when all images with data-image-id have been loaded.

Usage:

document.addEventListener('balancy-images-complete', () => {
  console.log('All images loaded');
  // Hide loading spinner
  hideLoadingSpinner();
});

balancy-localization-complete

Fired when all elements with data-text-type="localized" have been localized.

Usage:

document.addEventListener('balancy-localization-complete', () => {
  console.log('All text localized');
  // Adjust layouts based on text length
  adjustLayouts();
});

Other Lifecycle Events

// Buttons processed
document.addEventListener('balancy-buttons-complete', () => {
  console.log('All buttons ready');
});

// Dynamic text updated
document.addEventListener('balancy-text-complete', () => {
  console.log('All dynamic text updated');
});

// Fonts loaded
document.addEventListener('balancy-fonts-complete', () => {
  console.log('All fonts loaded');
});

// Audio prepared
document.addEventListener('balancy-audio-complete', () => {
  console.log('All audio ready');
});

Component Lifecycle Events (New!)

When using the component system, scripts have lifecycle methods that are called automatically:

awake()

Called immediately when component is created, after serialized parameters are applied.

class MyScript extends balancy.ElementBehaviour {
  // @serialize {number}
  maxHealth = 100;

  awake() {
    // Parameters already applied
    console.log('Max health:', this.maxHealth);

    // Set up event listeners
    this.element.addEventListener('click', () => this.onClick());

    // Initialize state
    this.currentHealth = this.maxHealth;
  }
}

start()

Called once before the first update(), after all components have called awake().

class MyScript extends balancy.ElementBehaviour {
  start() {
    // Safe to get references to other components
    this.healthBar = this.getComponent(HealthBar);

    // Start animations
    this.startAnimation();
  }
}

update(deltaTime)

Called every frame (typically 60 FPS). deltaTime is in seconds.

class MyScript extends balancy.ElementBehaviour {
  update(deltaTime) {
    // Update position
    this.position += this.velocity * deltaTime;

    // Update timer
    this.timer -= deltaTime;
    if (this.timer <= 0) {
      this.onTimerExpired();
    }
  }
}

onEnable() / onDisable()

Called when component or element becomes active/inactive.

class MyScript extends balancy.ElementBehaviour {
  onEnable() {
    console.log('Component enabled');
    this.startTimer();
  }

  onDisable() {
    console.log('Component disabled');
    this.stopTimer();
  }
}

Triggered by:

  • balancy.ElementsManager.setActive(element, true/false)
  • component.enabled = true/false
  • Parent element visibility changes

onDestroy()

Called when element is being destroyed.

class MyScript extends balancy.ElementBehaviour {
  onDestroy() {
    // Clean up timers
    if (this.timer) clearInterval(this.timer);

    // Remove event listeners
    this.element.removeEventListener('click', this.clickHandler);

    // Release references
    this.targetElement = null;
  }
}

Learn More: Prefabs & Components


Button Interaction Events

balancyButtonResponse

Fired when a button action completes (success or failure).

Event Detail:

{
  actionId: number,      // Action ID from RequestAction
  result: any,           // Response data
  success: boolean,      // Whether action succeeded
  senderId: string,      // Element ID that triggered action
  error?: string         // Error message (if failed)
}

Usage:

const button = document.getElementById('buy-button');

button.addEventListener('balancyButtonResponse', (event) => {
  const { actionId, result, success, senderId } = event.detail;

  if (success) {
    console.log(`Action ${actionId} succeeded:`, result);
    showSuccessAnimation();
    updatePlayerCurrency();
  } else {
    console.log(`Action ${actionId} failed:`, event.detail.error);
    showErrorDialog(event.detail.error);
  }
});

balancyButtonError

Fired when a button action encounters an error before execution.

Event Detail:

{
  actionId: number,
  error: string,
  paramsAttr: string     // The data-button-params value
}

Usage:

button.addEventListener('balancyButtonError', (event) => {
  const { actionId, error, paramsAttr } = event.detail;
  console.error(`Button error: ${error}`);
  console.error(`Action: ${actionId}, Params: ${paramsAttr}`);

  // Show user-friendly error
  showErrorToast('Something went wrong. Please try again.');
});

Example: Complete Purchase Flow

const buyButton = document.getElementById('buy-gems-button');

// Handle successful/failed purchases
buyButton.addEventListener('balancyButtonResponse', (event) => {
  if (event.detail.success) {
    // Purchase succeeded
    const gems = event.detail.result.gemsAwarded;

    // Show success
    showSuccessMessage(`You got ${gems} gems!`);

    // Update UI
    updateGemsDisplay();

    // Play sound
    playSound('purchase_success');
  } else {
    // Purchase failed
    showErrorDialog(event.detail.error);
  }
});

// Handle errors
buyButton.addEventListener('balancyButtonError', (event) => {
  console.error('Purchase error:', event.detail.error);
  showErrorDialog('Purchase failed. Please try again.');
});

Backend Notification Events

balancy-notification

Custom event dispatched for all backend notifications.

Usage:

window.addEventListener('balancy-notification', (event) => {
  const data = event.detail;
  console.log('Backend notification:', data);

  switch (data.type) {
    case balancy.NotificationType.OnOfferDeactivated:
      handleOfferDeactivated(data);
      break;
    case balancy.NotificationType.OnOfferGroupWasPurchased:
      handleOfferPurchased(data);
      break;
  }
});

Notification Types

balancy.NotificationType = {
  OnOfferDeactivated: 101,          // Offer expired/deactivated
  OnOfferGroupDeactivated: 102,     // Group offer expired
  OnOfferGroupWasPurchased: 122    // Group offer purchased
};

Override Notification Handler

You can override the default notification handler:

// Save original handler
const originalHandler = balancy.notificationReceived;

balancy.notificationReceived = function(data) {
  console.log('Notification received:', data);

  // Custom logic
  if (data.type === balancy.NotificationType.OnOfferDeactivated) {
    if (window.balancyViewOwner.instanceId === data.id) {
      // Close view if this offer expired
      balancy.closeView('Offer expired');
    }
  }

  // Call original handler
  originalHandler.call(this, data);

  // Or handle completely custom
  if (data.type === balancy.NotificationType.OnOfferGroupWasPurchased) {
    refreshOfferList();
    showCelebrationAnimation();
  }
};

Example: Auto-close on Expiration

window.addEventListener('balancy-notification', (event) => {
  const data = event.detail;

  // Close view if current offer expired
  if (data.type === balancy.NotificationType.OnOfferDeactivated) {
    if (window.balancyViewOwner.instanceId === data.id) {
      // Show expiration message
      showToast('This offer has expired');

      // Close after delay
      setTimeout(() => {
        balancy.closeView('Offer expired');
      }, 2000);
    }
  }
});

Custom Message Events

Balancy provides two-way communication between your game code and Views. For complete documentation including SDK-side setup (intercepting messages, sending responses, broadcasting), see Working with Views - Custom Messages.

balancy-custom-message

Fired when receiving custom messages from the SDK.

Usage:

// Method 1: DOM event
window.addEventListener('balancy-custom-message', (event) => {
  const data = event.detail;
  console.log('Custom message:', data);

  if (data.action === 'score-update') {
    updateScoreDisplay(data.score);
  }
});

// Method 2: Subscribe with callback (recommended)
const unsubscribe = balancy.subscribeToCustomMessages((data) => {
  console.log('Custom message:', data);

  switch (data.action) {
    case 'score-update':
      updateScoreDisplay(data.score);
      break;
    case 'achievement':
      showAchievementPopup(data.achievementId);
      break;
  }
});

// Unsubscribe when done
unsubscribe();

Multiple Subscribers:

// Analytics subscriber
balancy.subscribeToCustomMessages((data) => {
  if (data.action === 'track-event') {
    analytics.track(data.eventName, data.properties);
  }
});

// UI subscriber
balancy.subscribeToCustomMessages((data) => {
  if (data.action === 'update-ui') {
    refreshUI();
  }
});

// Both subscribers receive all messages

SDK Setup:

To send messages from your game code to Views, you need to use Balancy.Actions.View.SendCustomMessageToView(). For complete examples including request-response patterns and message interception, see Custom Messages Documentation.


Event Best Practices

1. Use once Flag for Initialization

// ✓ Good: Automatically removes listener after first call
window.addEventListener('balancy-ready', main, { once: true });

// ✗ Avoid: Listener stays attached
window.addEventListener('balancy-ready', main);

2. Clean Up Event Listeners

class MyComponent extends balancy.ElementBehaviour {
  awake() {
    this.clickHandler = () => this.onClick();
    this.element.addEventListener('click', this.clickHandler);
  }

  onDestroy() {
    // ✓ Good: Remove listener
    this.element.removeEventListener('click', this.clickHandler);
  }
}

3. Handle Errors Gracefully

// ✓ Good: Handle both success and error
button.addEventListener('balancyButtonResponse', (event) => {
  if (event.detail.success) {
    handleSuccess(event.detail.result);
  } else {
    handleError(event.detail.error);
  }
});

button.addEventListener('balancyButtonError', (event) => {
  handleError(event.detail.error);
});

// ✗ Avoid: Only handling success
button.addEventListener('balancyButtonResponse', (event) => {
  handleSuccess(event.detail.result);  // Will break on errors!
});

4. Coordinate Async Operations

// ✓ Good: Wait for multiple events
let imagesReady = false;
let componentsReady = false;

document.addEventListener('balancy-images-complete', () => {
  imagesReady = true;
  checkAllReady();
}, { once: true });

document.addEventListener('balancy-components-complete', () => {
  componentsReady = true;
  checkAllReady();
}, { once: true });

function checkAllReady() {
  if (imagesReady && componentsReady) {
    startGame();
  }
}

5. Use Custom Events for Communication

// ✓ Good: Decouple components with custom events
class ScoreManager extends balancy.ElementBehaviour {
  addScore(points) {
    this.score += points;

    // Dispatch custom event
    const event = new CustomEvent('score-changed', {
      detail: { score: this.score, points }
    });
    window.dispatchEvent(event);
  }
}

class ScoreDisplay extends balancy.ElementBehaviour {
  awake() {
    window.addEventListener('score-changed', (event) => {
      this.updateDisplay(event.detail.score);
    });
  }
}

Common Patterns

Pattern 1: Initialization Sequence

async function main() {
  balancy.delayIsReady();

  try {
    // Load data in parallel
    const [offers, playerData] = await Promise.all([
      balancy.getActiveOffers(),
      balancy.getSystemProfileValue('GeneralInfo')
    ]);

    // Initialize UI
    displayOffers(offers);
    displayPlayerInfo(playerData);

    // Signal ready
    balancy.sendIsReady();
  } catch (error) {
    console.error('Initialization failed:', error);
    balancy.closeView('Initialization error');
  }
}

window.addEventListener('balancy-ready', main, { once: true });

Pattern 2: Timer with Expiration

class OfferTimer extends balancy.ElementBehaviour {
  // @serialize {element}
  timerText = null;

  _updateInterval = 1.0;
  _timeSinceUpdate = 0;

  update(deltaTime) {
    this._timeSinceUpdate += deltaTime;

    if (this._timeSinceUpdate >= this._updateInterval) {
      this._timeSinceUpdate = 0;
      this.updateTimer();
    }
  }

  updateTimer() {
    const timeLeft = balancy.getTimeLeft();
    this.timerText.element.textContent = balancy.formatTime(timeLeft);

    if (timeLeft <= 0) {
      // Dispatch expiration event
      const event = new CustomEvent('timer-expired');
      window.dispatchEvent(event);

      // Close view
      balancy.closeView('Timer expired');
    }
  }
}

Pattern 3: Purchase Flow with State

class PurchaseManager extends balancy.ElementBehaviour {
  // @serialize {element}
  buyButton = null;

  awake() {
    this.buyButton.element.addEventListener('balancyButtonResponse',
      (e) => this.handlePurchase(e));
  }

  handlePurchase(event) {
    if (event.detail.success) {
      // Dispatch success event
      const successEvent = new CustomEvent('purchase-success', {
        detail: event.detail.result
      });
      window.dispatchEvent(successEvent);

      // Update button state
      const buttonScript = this.buyButton.getComponent(Balancy_UI_Common_BuyButton);
      buttonScript.setState(balancy.BuyButtonState.Cooldown, {
        cooldownSeconds: 3600
      });
    } else {
      // Dispatch error event
      const errorEvent = new CustomEvent('purchase-error', {
        detail: { error: event.detail.error }
      });
      window.dispatchEvent(errorEvent);
    }
  }
}

// Listen for purchase events
window.addEventListener('purchase-success', (event) => {
  showSuccessAnimation();
  updatePlayerCurrency();
});

window.addEventListener('purchase-error', (event) => {
  showErrorDialog(event.detail.error);
});

Troubleshooting

Events Not Firing

Problem: Event listener never called.

Solutions:

  • Check event name spelling
  • Verify listener added before event fires
  • Use { once: true } carefully (listener removed after first call)
  • Check browser console for errors

Multiple Event Calls

Problem: Event handler called multiple times.

Solutions:

  • Use { once: true } flag for initialization events
  • Remove listeners in onDestroy()
  • Check if listener added multiple times
  • Use event delegation for dynamic elements

Race Conditions

Problem: Events fire in unexpected order.

Solutions:

  • Don't assume event order
  • Use flags to track state
  • Wait for specific combination of events
  • Use balancy.delayIsReady() for async operations

Next Steps