Skip to content

Balancy UI Builder Documentation

Table of Contents

  1. Introduction
  2. UI Builder
  3. JavaScript API Reference
  4. Data Attributes System
  5. Events System
  6. Global Variables
  7. Common Patterns
  8. Battle Pass Integration

Introduction

The Balancy UI Builder allows developers to create custom user interfaces using HTML, CSS, and JavaScript. The system automatically injects a powerful JavaScript API that provides seamless integration with the Balancy backend, automatic element management, and cross-platform communication.

UI Builder

UI Builder is available for editing specific type of Assets called Views. To open UI Builder, go to Assets page and select View tab. The page will show Views you currently have in your project and Create button to create a new one. In the context menu of a View there is Open action, as it is shown here:

UI Builder opening

In general, UI Builder will look like this:

UI Builder main layout

It has main working area with the View's layout, tool panel on the right with basic elements, navigation panel with all the content of the View, and the tool panel on the top with additional controls.

What is View?

View is a specific type of asset combining HTML, CSS, and JavaScript code. Views are user interfaces that can be used to visualise game events and offers of any kind. Views are being opened in Webview provided and controlled by Balancy SDK.

Tools Panel

Located in the right side, it provides a collection of basic elements to use as building blocks for Views, allows to set up element's style, and to manage additional settings for elements, such as dynamic data or information to connect them to JavaScript.

All three Tabs are shown here:

UI Builder main layout

Basic blocks are:

  • Containers - many combinations, needed to build adaptive layouts
  • Images - static art assets, connected to Balancy resurces. Regular or 9-sliced
  • Texts - static, localized, and dynamic texts
  • Buttons - always 9-sliced images with an option to assign actions

For selected element tool panel will show styling params to control position, layout constraints, size, typography, and additional effects.

For such elements like images and texts there are additional parameters on Settings tab, such as image source, text source, button action, string formatting, 9-slice image mapping, etc.

Hierarchy view

Located on the left side, navigation panel shows a tree view with all elements used in current View, providing the way to rearrange layout and select elements for editing. Elements can be moved between containers via simple drag-n-drop.

Top Panel

On the top panel there are following controls:

UI Builder main layout

In order from left to right:

  • Device screen size selector. Allows to select different screen aspect ratios typical for some popular devices.
  • Landscape toggle. Allows to switch the layout between portrait and landscape orientations.
  • Copy / Paste buttons. Allows to copy-paste elements selected in navigation, even between different Views.
  • Play UI button. Allows to quickly test changes by overriding the View currently loaded in Demo App (see below).
  • Save button. Nothing special, just saves changes.
  • View Components toggle. Allows to show/hide real boundaries of components, even invisible ones to check layouts.
  • View Code button. Allows to check HTML and CSS code generated during the editting. Read-only.
  • Global Script Editor button. Opens code editor for the global JavaScript code of the View. See below.
  • Add Fonts button. Opens the list of custom fonts, added to support proper visuals of the View. Works with fonts uploaded on Assets/Fonts tab.

Global Script

Global Script is the way to embed JavaScript code into View. It may be used for creating animations and for working with dynamic content of the View, that depends on the individual player progress. Examples: particle animations, moving elements, playing sounds, actual progress through Season Pass, ect.

Script Editor has Find function (Ctrl-F / Cmd-F) and Replace function (Ctrl-H / Cmd-H).

Here is an example of clock animation for the timer of Season Pass View, provided by Balancy:

UI Builder main layout

See the full documentation on JS API below in this document.

Previewing in Demo App

It is possible to use built-in Demo App to check recent changes of the View in real time, without having to deploy changes in the regular way.

UI Builder main layout

To do so:

  • Have Game Event properly set up on the Dashboard, so the Demo App will be able to activate it for you based on Condition. You will need your View to be assigned to their respective entities - simple offer, group offer or game event itself.
  • While being inside UI Builder, open Demo App by clicking >> button on the top panel near the User Account icon.
  • Save changes made for the View by Save button.
  • Press Play UI button. It will load updated View into the Demo App bypassing Deploy process. It will show notification in case of success.
  • Open the entity of your View in Demo App UI.
  • When the View is opened this way, it will show Overridden View: v1 text at the bottom of it, so it will be clear that the content is actually taken directly from UI Builder, and not from the latest Deploy. Version number in vX will increase automatically after every use of Play UI button during current working session.

Using 9-slice Images

To use 9-slice images in your Views, you need to set them up as such in Assets/Images section first:

UI Builder main layout

Then you will add Image 9-slice or Button 9-slice element to your View. It will open assets selector to choose your image from the list:

UI Builder main layout

After that, specific parameters of your 9-slice will be applied to the element automatically.

UI Builder main layout

You can change the image later by pressing Select Image button in the Settings tab of Tool Panel to open assets selector again.

Using Localized Texts

To use Localized Text in your Views, you need to have it created in Localization section first. Then you will add Text element to your View and on the Settings tab of Tool Panel you will press Select Local String button.

UI Builder main layout

It will open Localized String selector:

UI Builder main layout

This way your Views will always be localized the same way as any other of your game configs.

Using Dynamic Texts

To use text elements that display dynamic information such as times, prices, titles, descriptions that would be taken from the Offers/Events assiciated with your View, set the type of Text element to Dynamic. After that, follow API documentation below, depending on what type of information you need to display.

Example of using dynamic text for offer duration timer:

UI Builder main layout

JavaScript API Reference

The Balancy framework automatically injects the balancy object into your JavaScript environment, providing access to backend data, user profiles, and platform-specific functionality.


balancy.findDomElement(dataId, parentElement?)

Finds an element with the specified data-id attribute.

Parameters:

  • dataId (string): The data-id value to search for
  • parentElement (HTMLElement, optional): Parent element to search within (defaults to document)

Returns: HTMLElement | null

Example:

const headerElement = balancy.findDomElement('window-header');
const button = balancy.findDomElement('claim-button', headerElement);

balancy.setImage(element, sprite)

Sets an image on an HTML element using a Balancy sprite object.

Parameters:

  • element (HTMLElement): Target element (IMG tag or element with background)
  • sprite (object): Sprite object with id property

Example:

const iconElement = document.getElementById('reward-icon');
balancy.setImage(iconElement, reward.item.unnyIcon);


balancy.getSystemProfileValue(path)

Retrieves a value from the system profile (UnnyProfile).

Parameters:

  • path (string): Dot-separated path to the desired value

Returns: Promise<any>

Example:

const playerLevel = await balancy.getSystemProfileValue('GeneralInfo.Level');
//playerLevel = 5

const generalInfo = await balancy.getSystemProfileValue('GeneralInfo');
//generalInfo example (Object)
// {
//     "level": 0,
//     "country": "CY",
//     "session": 68,
//     "customId": "Custom456",
//     "deviceId": "30438f52-d81e-4065-8607-298c98626ecc",
//     "platform": "Unknown",
//     "playTime": 18295,
//     "isNewUser": false,
//     "profileId": "528c0854-54d8-11f0-b905-1fec53a055ba",
//     "appVersion": "1.0.0",
//     "currentDay": 20297,
//     "deviceName": "",
//     "deviceType": 0,
//     "platformId": -1,
//     "deviceModel": "",
//     "dontDisturb": false,
//     "offlineTime": 1,
//     "tutorialStep": 0,
//     "engineVersion": "React_1.0",
//     "lastLoginTime": 1753712746,
//     "trafficSource": "",
//     "firstLoginTime": 1753186128,
//     "installVersion": "1.0.0",
//     "systemLanguage": "",
//     "operatingSystem": "",
//     "trafficCampaign": "",
//     "gameLocalization": "en",
//     "systemMemorySize": 0,
//     "timeSinceInstall": 526622,
//     "balancyPlatformId": 7,
//     "timeSincePurchase": 338642,
//     "operatingSystemFamily": 0
// }

balancy.getProfileValue(profile, path)

Retrieves a value from a specific profile.

Parameters:

  • profile (string): Profile name
  • path (string): Dot-separated path to the desired value

Returns: Promise<any>

Example:

const value = await balancy.getProfileValue('PlayerData', 'Achievements.Completed');

balancy.getDocumentValue(id, depth)

Retrieves a document from Balancy with specified depth.

Parameters:

  • id (string): Document ID
  • depth (number): Depth of nested objects to fetch

Returns: Promise<object>

Example:

const config = await balancy.getDocumentValue('245', 3);
console.info(config);


balancy.getLocalizedText(key)

Gets localized text for the specified key.

Parameters:

  • key (string): Localization key

Returns: Promise<string>

Example:

const welcomeText = await balancy.getLocalizedText('welcome_message');
document.getElementById('title').textContent = welcomeText;

balancy.getImageUrl(id)

Gets the URL for an image asset.

Parameters:

  • id (string): Image asset ID

Returns: Promise

Example:

const imageUrl = await balancy.getImageUrl('123');
document.getElementById('icon').src = imageUrl;


balancy.closeView(source)

Closes the current view.

Parameters:

  • source (string): Reason for closing (for debugging)

Example:

balancy.closeView('Purchase completed');

balancy.sendIsReady()

While loading a page, Balancy automatically injects sprites and fonts, once everything is ready it shoots balancy-ready event. Right after this event Balance View notifies the SDK that it's ready and is being shown to the user. If you want to postpone and load any additional data, you can delay the ready signal:

Example:

function main() {
    balancy.delayIsReady();

    //some async logic...., then
    balancy.sendIsReady();
}
window.addEventListener('balancy-ready', main, { once: true });


balancy.getBattlePassConfig()

Gets the configuration for the current battle pass.

Returns: Promise<object> - Battle pass configuration

Example:

const config = await balancy.getBattlePassConfig();
console.log('Scores required:', config.scores);
console.log('Reward lines:', config.rewards);

Response Structure:

{
    unnyId: "995",
    name: "LIVEOPS TEMPLATES/KEY_SEASON_PASS_NAME",
    scores: [10, 20, 30, 40, 50, 60], // XP required for each level
    rewards: [
      {
        unnyId: "1007",
        name: "Free Track",
        rewards: [
          {
            unnyId: "1002",
            count: 1,
            item: {
              unnyId: "826",
              name: "Gems",
              unnyIcon: { id: "819", type: 1 }
            }
          }
          // ... more rewards
        ]
      }
      // ... more reward lines
    ]
}

balancy.getBattlePassProgress()

Gets the current player's battle pass progress.

Returns: Promise<object> - Battle pass progress

Example:

const progress = await balancy.getBattlePassProgress();
console.log('Current level:', progress.level);
console.log('Current XP:', progress.scores);

Response Structure:

{
  level: 1,           // Current level (0-based)
  scores: 15,         // Current XP/scores
  finished: false,    // Whether battle pass is completed
  progressInfo: [
    {
      progress: [1, 0, 0, 0, 0, 0], // Claim status for each level (0=locked, 1=available, 2=claimed)
      available: true,               // Whether this reward line is available
      unnyIdReward: "1007"          // Reward line ID
    }
    // ... more reward lines
  ]
}

balancy.claimBattlePassReward(lineId, index)

Claims a battle pass reward.

Parameters:

  • lineId (string): Reward line ID
  • index (number): Level index to claim

Returns: Promise<boolean> - Success status

Example:

const success = await balancy.claimBattlePassReward('1007', 0);
if (success) {
  console.log('Reward claimed successfully!');
}


balancy.canBuyGroupOffer(index)

Checks if a group offer can be purchased.

Parameters:

  • index (number): Offer index

Returns: Promise<boolean>

Example:

const canBuy = await balancy.canBuyGroupOffer(0);
document.getElementById('buy-button').disabled = !canBuy;


balancy.formatTime(seconds)

Formats time duration into a human-readable string.

Parameters:

  • seconds (number): Duration in seconds

Returns: string

Example:

const timeLeft = balancy.getTimeLeft();
const formatted = balancy.formatTime(timeLeft);
// Returns: "2d 5h" or "01:23:45"

balancy.getTimeLeft()

Gets remaining time based on balancySettings.

Returns: number - Seconds remaining

Example:

const timeLeft = balancy.getTimeLeft();
document.getElementById('timer').textContent = balancy.formatTime(timeLeft);

balancy.formatDataTemplate(formatStr, data)

Formats a template string with data values. This powerful method supports nested object access and automatic document resolution using unnyId references.

Parameters:

  • formatStr (string): Template string with {path} placeholders
  • data (object): Data object

Returns: Promise

Example - Basic Usage:

const template = "You have {currency.coins} coins and {level} level";
const data = { currency: { coins: 100 }, level: 5 };
const result = await balancy.formatDataTemplate(template, data);
// Returns: "You have 100 coins and 5 level"

Example - UnnyId Resolution:

// If data.gameOffer is null but data.unnyIdGameOffer exists,
// the method will automatically fetch the document
const template = "{gameOffer.name}";
const data = {
  gameOffer: null,
  unnyIdGameOffer: "1188"
};
const result = await balancy.formatDataTemplate(template, data);
// Returns: "DEFAULT/offer_name_key"


Constants and Enums

RequestAction

const RequestAction = {
  None: 0,
  GetProfile: 1,
  SetProfile: 2,
  GetDocument: 3,
  GetLocalization: 10,
  GetImageUrl: 11,
  GetInfo: 12,
  CanBuyGroupOffer: 13,
  GetBattlePassProgress: 20,
  BuyOffer: 101,
  BuyGroupOffer: 102,
  BuyShopSlot: 103,
  BattlePassClaim: 104,
  CloseWindow: 200,
  BalancyIsReady: 201,
  CustomMessage: 1000
};

InfoType

const InfoType = {
  None: 0,
  OfferPrice: 1,
  OfferGroupPrice: 2,
  TimeLeft: 3,
  OwnerLocalizedKey: 4,
  OwnerCustom: 5,
  CustomPrice: 9,
  Custom: 10
};

BattlePassStatus

const BattlePassStatus = {
  NotAvailable: 0,
  Available: 1,
  Claimed: 2
};

Data Attributes System

Balancy automatically processes HTML elements with special data attributes, providing declarative functionality without JavaScript code.

Button Actions

Add data-button-action to make any element interactive:

<!-- Buy offer button -->
<button data-button-action="101" data-button-params='{"productId": "coin_pack_1"}'>
  Buy Coins
</button>

<!-- Buy group offer -->
<button data-button-action="102" data-index="0">
  Buy Special Pack
</button>

<!-- Claim battle pass reward -->
<button data-button-action="104" data-index="2">
  Claim Reward
</button>

Available Attributes:

  • data-button-action: Action ID from RequestAction enum
  • data-button-params: JSON string with additional parameters
  • data-index: Index parameter for certain actions

Button Events:

  • balancyButtonResponse: Fired when action completes
  • balancyButtonError: Fired on error

Example Event Handling:

button.addEventListener('balancyButtonResponse', (event) => {
  const { success, result, actionId } = event.detail;
  if (success) {
    console.log('Action completed:', result);
  }
});

Dynamic Text Updates

Elements with data-text-type="dynamic" automatically update with backend data:

<!-- Show offer price -->
<span data-text-type="dynamic" 
      data-info-type="1" 
      data-text-format="{price} USD">
</span>

<!-- Show time remaining -->
<span data-text-type="dynamic" 
      data-info-type="3" 
      data-text-format="Time left: {time}">
</span>

<!-- Owner custom data formatting -->
<span data-text-type="dynamic" 
      data-info-type="5" 
      data-text-format="{discount}% OFF - ${priceUSD}">
</span>

<!-- Owner localized key -->
<span data-text-type="dynamic" 
      data-info-type="4" 
      data-text-format="{gameOffer.name}">
</span>

<!-- Custom data -->
<span data-text-type="dynamic" 
      data-info-type="10" 
      data-custom="player_stats"
      data-text-format="Level {level} - {xp} XP">
</span>

Available Attributes:

  • data-text-type="dynamic": Marks element for dynamic updates
  • data-info-type: Type of information to fetch (InfoType enum)
  • data-text-format: Template string for formatting
  • data-custom: Custom parameter for InfoType.Custom
  • data-product-id: Product ID for price queries
  • data-index: Index for group offers

InfoType Values:

  • 1 (OfferPrice): Display offer price
  • 2 (OfferGroupPrice): Display group offer price
  • 3 (TimeLeft): Display time remaining (automatically formatted)
  • 4 (OwnerLocalizedKey): Format template using owner data, then use result as localization key
  • 5 (OwnerCustom): Format template using owner data directly
  • 9 (CustomPrice): Display custom price information
  • 10 (Custom): Display custom data with specified parameter

Advanced InfoTypes: OwnerLocalizedKey and OwnerCustom

These new InfoTypes work with the current view's owner data (window.balancyViewOwner) and support automatic document resolution:

InfoType.OwnerLocalizedKey (4): Formats the template using owner data, then uses the result as a localization key.

<!-- If owner has gameOffer.name = "OFFER_SUPER_PACK_NAME" -->
<span data-text-type="dynamic" 
      data-info-type="4" 
      data-text-format="{gameOffer.name}">
</span>
<!-- Will display the localized text for key "OFFER_SUPER_PACK_NAME" -->

InfoType.OwnerCustom (5): Formats the template using owner data directly for display.

<!-- Direct formatting with owner data (avoid using localized keys like gameOffer.name) -->
<span data-text-type="dynamic" 
      data-info-type="5" 
      data-text-format="{discount}% OFF - {priceUSD} USD">
</span>
<!-- Will display: "25% OFF - 4.99 USD" -->

Owner Data Structure Example:

window.balancyViewOwner = {
  instanceId: "f09ab140-3752-4593-98e3-48a31046",
  gameOffer: null,                    // Will be resolved automatically
  unnyIdGameOffer: "1188",           // Used for document resolution
  discount: 25,
  priceUSD: 4.99,
  status: 1
  // ... other fields
};

Automatic Document Resolution: Both InfoTypes automatically resolve null references using unnyId fields:

  • If {gameOffer.name} is accessed but gameOffer is null
  • The system looks for unnyIdGameOffer in the same object
  • Fetches the document using balancy.getDocumentValue()
  • Continues with the remaining path (name in this case)

This works recursively for nested paths like {gameOffer.storeItem.name}.

Important Usage Guidelines:

  • Use InfoType.OwnerLocalizedKey (4) when you need to display localized text. Fields like gameOffer.name contain localization keys, not display text.
  • Use InfoType.OwnerCustom (5) for non-localized data like numbers, counts, prices, and other raw values.
  • Avoid mixing localization keys with raw data in InfoType.OwnerCustom - if you need {gameOffer.name} as display text, use InfoType.OwnerLocalizedKey instead.

Correct Pattern:

<!-- For localized title -->
<h1 data-text-type="dynamic" data-info-type="4" data-text-format="{gameOffer.name}"></h1>

<!-- For price and discount -->
<span data-text-type="dynamic" data-info-type="5" data-text-format="{discount}% OFF"></span>
<span data-text-type="dynamic" data-info-type="5" data-text-format="${priceUSD}"></span>

Incorrect Pattern:

<!-- DON'T do this - gameOffer.name is a localization key, not display text -->
<span data-text-type="dynamic" data-info-type="5" data-text-format="{gameOffer.name} - {discount}% OFF"></span>

Automatic Localization

Elements with data-text-type="localized" automatically get localized:

<h1 data-text-type="localized" data-localization-key="welcome_title">
  <!-- Will be replaced with localized text -->
</h1>

<button data-text-type="localized" data-localization-key="buy_button">
  Buy Now
</button>

Automatic Image Loading

Elements with data-image-id automatically load images:

<!-- For IMG elements -->
<img data-image-id="icon_123" alt="Reward Icon">

<!-- For background images -->
<div data-image-id="background_456" class="hero-section"></div>

Custom Font Injection

Elements with font data attributes automatically load custom fonts:

<div data-font-id="font_789" data-font-name="CustomFont" class="styled-text">
  This text uses a custom font
</div>

Element Identification

Use data-id for element identification:

<div data-id="player-info">
  <span data-id="player-name">Player Name</span>
  <span data-id="player-level">Level 1</span>
</div>

<script>
const playerInfo = balancy.findDomElement('player-info');
const nameElement = balancy.findDomElement('player-name', playerInfo);
</script>

Events System

Lifecycle Events

The framework dispatches these events during initialization:

  • balancy-buttons-complete: All buttons are processed
  • balancy-localization-complete: All text is localized
  • balancy-text-complete: All dynamic text is updated
  • balancy-images-complete: All images are loaded
  • balancy-fonts-complete: All fonts are loaded
  • balancy-ready: Everything is ready (dispatched after all above)

Example:

document.addEventListener('balancy-ready', () => {
  console.log('Balancy UI is fully initialized');
  // Your custom initialization code here
});

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

Button Interaction Events

balancyButtonResponse Event:

button.addEventListener('balancyButtonResponse', (event) => {
  const { actionId, result, success, senderId } = event.detail;
  if (success) {
    console.log(`Action ${actionId} succeeded:`, result);
  } else {
    console.log(`Action ${actionId} failed:`, event.detail.error);
  }
});

balancyButtonError Event:

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

Backend Notifications

The framework automatically handles notifications from the backend:

// This is handled automatically, but you can override:
balancy.notificationReceived = function(data) {
  console.log('Notification received:', data);

  switch (data.type) {
    case 101: // OnOfferDeactivated
      if (window.balancyViewOwner.instanceId === data.id) {
        balancy.closeView('Offer expired');
      }
      break;
    case 122: // OnOfferGroupWasPurchased
      // Refresh UI state
      updateOfferAvailability();
      break;
  }
};

Global Variables

window.balancyViewOwner

Contains context information about the current view:

{
  instanceId: "offer_123",      // Current offer/event instance ID
  unnyIdGameEvent: "663",       // Game event ID (for battle pass, etc.)
  productId: "coin_pack_small"  // Product ID for purchases
}

window.balancySettings

Contains view settings and timing information:

{
  launchTime: 1751328000,  // Unix timestamp when view was launched
  secondsLeft: 86400       // Seconds remaining for time-limited content
}

Custom Function Overrides

You can override certain behaviors:

// Custom time formatting
window.customFormatTime = function(seconds) {
  if (seconds < 60) return `${seconds}s`;
  if (seconds < 3600) return `${Math.floor(seconds/60)}m`;
  return `${Math.floor(seconds/3600)}h`;
};

Common Patterns

Loading and Displaying Data

async function loadPlayerInfo() {
  try {
    const level = await balancy.getSystemProfileValue('GeneralInfo.Level');
    const coins = await balancy.getProfileValue('Profile', 'Currency.Coins');
    const playerName = await balancy.getProfileValue('Profile', 'GeneralInfo.Name');

    document.getElementById('level').textContent = level;
    document.getElementById('coins').textContent = coins;
    document.getElementById('name').textContent = playerName;
  } catch (error) {
    console.error('Failed to load player info:', error);
  }
}

Creating Interactive Buttons

function createOfferButton(offer) {
  const button = document.createElement('button');
  button.setAttribute('data-button-action', '101'); // BuyOffer
  button.setAttribute('data-button-params', JSON.stringify({
    productId: offer.productId,
    currency: 'USD'
  }));

  button.addEventListener('balancyButtonResponse', (event) => {
    if (event.detail.success) {
      showPurchaseSuccess(offer);
    } else {
      showPurchaseError(event.detail.error);
    }
  });

  return button;
}

Timer Implementation

function startTimer() {
  function updateTimer() {
    const timeLeft = balancy.getTimeLeft();
    const formatted = balancy.formatTime(timeLeft);
    document.getElementById('timer').textContent = formatted;

    if (timeLeft <= 0) {
      balancy.closeView('Time expired');
      return;
    }
  }

  updateTimer();
  setInterval(updateTimer, 1000);
}

Handling Notifications

// Override notification handler for custom behavior
const originalNotificationHandler = balancy.notificationReceived;
balancy.notificationReceived = function(data) {
  // Call original handler
  originalNotificationHandler.call(this, data);

  // Add custom logic
  if (data.type === 122) { // OnOfferGroupWasPurchased
    showCelebrationAnimation();
    refreshOfferList();
  }
};

Working with Images and Localization

async function setupRewardDisplay(reward) {
  const container = document.getElementById('reward-container');

  // Create image element
  const img = document.createElement('img');
  balancy.setImage(img, reward.item.unnyIcon);

  // Create title with localization
  const title = document.createElement('h3');
  const localizedName = await balancy.getLocalizedText(reward.item.name);
  title.textContent = localizedName;

  // Create count display
  const count = document.createElement('span');
  count.textContent = `x${reward.count}`;

  container.appendChild(img);
  container.appendChild(title);
  container.appendChild(count);
}

Battle Pass Integration

The Battle Pass is a complete example of advanced Balancy integration. Here's a simplified version showing key concepts:

Basic Setup

async function initializeBattlePass() {
  // Delay automatic ready signal
  balancy.delayIsReady();

  try {
    // Load configuration and progress
    const config = await balancy.getBattlePassConfig();
    const progress = await balancy.getBattlePassProgress();

    // Setup UI
    setupHeader(progress, config.scores);
    setupRewardTrack(config.rewards, progress);
    setupClaimButtons(config.rewards, progress);

    // Notify ready
    balancy.sendIsReady();
  } catch (error) {
    console.error('Failed to initialize battle pass:', error);
  }
}

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

Progress Display

function updateProgressDisplay(progress, scores) {
  const currentLevel = progress.level;
  const currentScores = progress.scores;
  const maxScores = scores[currentLevel] || scores[scores.length - 1];
  const progressPercent = (currentScores / maxScores) * 100;

  document.getElementById('level').textContent = currentLevel + 1;
  document.getElementById('progress').style.width = `${progressPercent}%`;
  document.getElementById('scores').textContent = `${currentScores} / ${maxScores}`;
}

Reward Claiming

function setupClaimButton(lineId, levelIndex, reward, status) {
  const button = document.getElementById(`claim-${lineId}-${levelIndex}`);

  if (status === 1) { // Available
    button.style.display = 'block';
    button.onclick = async () => {
      const success = await balancy.claimBattlePassReward(lineId, levelIndex);
      if (success) {
        // Show reward animation
        showRewardAnimation(button, reward);
        // Update button state
        setupClaimButton(lineId, levelIndex, reward, 2); // Claimed
      }
    };
  } else {
    button.style.display = 'none';
  }
}

Animation System

function showRewardAnimation(sourceElement, reward) {
  const flyingDiv = document.createElement('div');
  flyingDiv.className = 'flying-reward';

  // Position at source element
  const rect = sourceElement.getBoundingClientRect();
  flyingDiv.style.left = rect.left + 'px';
  flyingDiv.style.top = rect.top + 'px';

  // Add reward icon
  const icon = document.createElement('img');
  balancy.setImage(icon, reward.item.unnyIcon);
  flyingDiv.appendChild(icon);

  // Add to page and animate
  document.body.appendChild(flyingDiv);

  // Animate upward and fade out
  flyingDiv.animate([
    { transform: 'translateY(0px)', opacity: 1 },
    { transform: 'translateY(-100px)', opacity: 0 }
  ], {
    duration: 2000,
    easing: 'ease-out'
  }).addEventListener('finish', () => {
    flyingDiv.remove();
  });
}

Advanced Usage Examples

Event Management in Practice

// Example: Managing event state and progress
async function initializeEvent() {
  // Load event configuration and custom data
  const eventConfig = await balancy.getDocumentValue(window.balancyViewOwner.unnyIdGameEvent, 3);
  const customEventInfo = await balancy.getCustomEventInfo();
  const tasks = await balancy.getTasks();

  // Initialize based on current state
  if (!customEventInfo.started) {
    await startNewEvent(eventConfig);
  } else {
    continueEvent(customEventInfo, tasks);
  }
}

async function startNewEvent(eventConfig) {
  // Save custom event data
  const customData = {
    started: true,
    startTime: Math.floor(Date.now() / 1000),
    progress: {}
  };

  await balancy.setCustomEventInfo(customData);

  // Activate event tasks
  const activeTasks = await balancy.activateTasks([eventConfig.task.unnyId]);
  console.log('Event started with tasks:', activeTasks);
}

async function completeEventTask(taskId) {
  const success = await balancy.claimTaskReward(taskId);
  if (success) {
    // Award inventory items
    await balancy.addInventoryItems('reward_coins', 100);
    console.log('Task completed and reward claimed');
  }
}

Group Offers and Purchasing

// Example: Chain deals implementation
async function setupChainOffers() {
  const offerId = window.balancyViewOwner.unnyIdGameOfferGroup;
  const offerGroup = await balancy.getDocumentValue(offerId, 3);
  const purchasedCount = window.balancyViewOwner.purchasedItems.length;

  // Check which offers are available
  for (let i = 0; i < offerGroup.storeItems.length; i++) {
    const canPurchase = await balancy.canBuyGroupOffer(i);
    updateOfferButton(i, canPurchase);
  }
}

async function purchaseOffer(index) {
  try {
    const result = await balancy.buyGroupOffer(index);
    console.log('Purchase successful:', result);
    // Update UI after purchase
    await setupChainOffers();
  } catch (error) {
    console.error('Purchase failed:', error);
  }
}

// Get product pricing information
async function displayOfferPrice(productId) {
  const productInfo = await balancy.getProductInfo(productId);
  document.getElementById('price').textContent = productInfo.LocalizedPriceString;
}

Inventory and Rewards Management

// Example: Managing player inventory
async function checkPlayerInventory() {
  const coinCount = await balancy.getInventoryItemsCount('coins');
  const gemCount = await balancy.getInventoryItemsCount('gems');

  console.log(`Player has ${coinCount} coins and ${gemCount} gems`);
}

async function awardEventReward(itemId, amount) {
  await balancy.addInventoryItems(itemId, amount);

  // Show reward notification
  showRewardNotification(itemId, amount);
}

async function spendCurrency(itemId, amount) {
  const currentCount = await balancy.getInventoryItemsCount(itemId);
  if (currentCount >= amount) {
    await balancy.removeInventoryItems(itemId, amount);
    return true;
  }
  return false;
}

Working with Item Display Formatting

// Example: Properly format item counts for display
function displayReward(reward) {
  const format = balancy.formatItemsCount(reward, true);

  const countElement = document.getElementById('reward-count');
  countElement.textContent = format.text;

  // Apply appropriate styling
  if (format.isCurrency) {
    countElement.classList.add('currency-style');
  } else if (format.isDuration) {
    countElement.classList.add('duration-style');
  }
}

// Format large numbers for display
function displayPlayerStats(xp, coins) {
  document.getElementById('xp').textContent = balancy.shortenCount(xp);
  document.getElementById('coins').textContent = balancy.shortenCount(coins);
}

// Template string formatting
function showMessage(template, ...values) {
  const message = balancy.formatString(template, ...values);
  document.getElementById('message').textContent = message;
}

// Usage: showMessage('You earned {0} coins and {1} XP!', 500, 150);

This documentation provides a comprehensive guide to the Balancy UI Builder's JavaScript capabilities. The system is designed to be both powerful for complex implementations and simple for basic use cases through its declarative data attribute system.