Skip to content

Working with Views

Overview

Views are Balancy assets that combine HTML, CSS, and JavaScript to create interactive user interfaces. This guide covers how to open views from your game code, pass data to them, and establish two-way communication between your game and the WebView.

Opening Views

Basic Usage

GameOffers and GameEvents have a UnnyView parameter. You can also create custom parameters of type Object with subtype View.

To open a view from your game code:

UnnyView.OpenView(onShownCallback, ownerObject);
unnyView.openView(onShownCallback, ownerObject);

Parameters

onShownCallback (optional): Callback function that will be called when the view is shown.

  • In most cases it's called instantly
  • May take time if elements need to be loaded (images, fonts, etc.)
  • Use for post-initialization logic

ownerObject (optional): Object that will be assigned to window.balancyViewOwner in the View's JavaScript context.

  • Pass additional data to the view
  • Usually used to pass GameEvent or GameOffer parameters
  • Accessible in JavaScript as window.balancyViewOwner

Example

// Open offer view
var offer = Balancy.LiveOps.Offers.GetActiveOffer("special_pack");
offer.UnnyView.OpenView(
    onShown: () => {
        Debug.Log("Offer view is now visible");
    },
    ownerObject: offer
);

// Open event view with custom data
var eventData = new Dictionary<string, object> {
    { "eventId", "summer_event" },
    { "progress", 75 },
    { "rewards", rewardsList }
};
myEvent.UnnyView.OpenView(null, eventData);
// Open offer view
const offer = Balancy.LiveOps.Offers.getActiveOffer("special_pack");
offer.unnyView.openView(
    () => {
        console.log("Offer view is now visible");
    },
    offer
);

// Open event view with custom data
const eventData = {
    eventId: "summer_event",
    progress: 75,
    rewards: rewardsList
};
myEvent.unnyView.openView(null, eventData);

Closing Views

From JavaScript

Views should include a close button that calls balancy.closeView():

// Close button handler
document.getElementById('close-btn').onclick = () => {
    balancy.closeView('User clicked close');
};

// Auto-close when timer expires
if (timeLeft <= 0) {
    balancy.closeView('Timer expired');
}

Automatic Close Button

Balancy automatically adds an invisible close button in the top-right corner of every view.

Purpose:

  • Provides a fallback if JavaScript crashes
  • Ensures users can always close the view
  • Cannot be disabled

Best Practice:

  • Always design your own visible close button
  • Position it clearly for users
  • Use consistent close button design across views

Passing Data to Views

Using window.balancyViewOwner

The ownerObject parameter becomes available as window.balancyViewOwner in JavaScript:

Game Code (C#):

var offerData = new {
    offerId = "summer_pack",
    discount = 50,
    originalPrice = 9.99,
    salePrice = 4.99
};

myView.OpenView(null, offerData);

View JavaScript:

window.addEventListener('balancy-ready', () => {
    const owner = window.balancyViewOwner;

    console.log('Offer ID:', owner.offerId);
    console.log('Discount:', owner.discount);

    // Update UI
    document.getElementById('discount').textContent = `${owner.discount}% OFF`;
    document.getElementById('price').textContent = `$${owner.salePrice}`;
});

Typical Owner Data Structure

For GameOffers:

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

For GameEvents:

window.balancyViewOwner = {
    unnyIdGameEvent: "663",
    eventName: "Summer Festival",
    startTime: 1751328000,
    endTime: 1751414400,
    // ... other event fields
};

Custom Messages

Balancy provides two-way communication between your game code and the WebView. This allows you to send messages back and forth, with request-response patterns and broadcasting.

Message Flow Overview

Game (C#/TypeScript)  ←→  WebView (JavaScript)
       ↓                        ↓
  SetOnMessageReceivedCallback  sendCustomMessage()
  SendResponseToView            subscribeToCustomMessages()
  sendCustomMessageToView       (event listeners)

JavaScript to SDK: Request-Response Pattern

Send custom messages from JavaScript and receive responses from your game code.

JavaScript Side

Send a message with balancy.sendCustomMessage():

balancy.sendCustomMessage('my_button', {
    param: 'myParam',
    value: 42
}).then(response => {
    console.info("Custom message response:", response);
}).catch(error => {
    console.error("Custom message error:", error);
});

Default Response:

Unless you override it, you'll get:

{
  "status": "ok"
}

SDK Side: Intercepting and Responding

Set up a message interceptor in your game code to catch messages and send custom responses:

[System.Serializable]
class MessageExample
{
    public string type;
    public string sender;
    public string id;
    public int action;
}

Balancy.Actions.View.SetOnMessageReceivedCallback((msg) =>
{
    //msg = {"type":"request","sender":"my_button","id":"58","action":1000,"params":{"param":"myParam","value":42}}
    var messageInfo = JsonUtility.FromJson<MessageExample>(msg);

    if (messageInfo.action == 1000)
    {
        // Send custom response back to JavaScript
        Balancy.Actions.View.SendResponseToView(
            messageInfo.id,
            "{\"response\":\"Hello from Unity!\"}"
        );
        return false;  // Don't let Balancy process this message
    }

    return true;  // Let Balancy handle other messages
});
interface MessageExample {
    type: string;
    sender: string;
    id: string;
    action: number;
}

Balancy.Actions.View.setOnMessageReceivedCallback((msg: string): boolean => {
    //msg = {"type":"request","sender":"my_button","id":"58","action":1000,"params":{"param":"myParam","value":42}}
    const messageInfo: MessageExample = JSON.parse(msg);

    if (messageInfo.action === 1000) {
        // Send custom response back to JavaScript
        Balancy.Actions.View.sendResponseToView(
            messageInfo.id,
            JSON.stringify({response: "Hello from TypeScript!"})
        );
        return false;  // Don't let Balancy process this message
    }

    return true;  // Let Balancy handle other messages
});

Important:

  • Return false after sending your custom response to prevent Balancy from sending the default response
  • Return true to let Balancy continue processing the message

Detecting View Closing

A common use case is detecting when a view is being closed. The close event is sent with action: 200 (RequestAction.CloseWindow).

Example Message Format

When the view is closing, you'll receive one of these:

{"action":200, "params":{}}

or

{"type":"request","sender":"ilmsd-2","id":"42","action":200,"params":{}}

Both indicate that the view is about to close.

Tracking View Closing

[System.Serializable]
class MessageExample
{
    public int action;
}

Balancy.Actions.View.SetOnMessageReceivedCallback((msg) =>
{
    var messageInfo = JsonUtility.FromJson<MessageExample>(msg);

    if (messageInfo.action == 200)
    {
        Debug.Log("View is closing!");
        // Perform any cleanup or tracking here
        // Log analytics event
        // Save player preferences
        // Resume game music
    }

    return true; // Continue processing - let Balancy close the view
});
interface MessageExample {
    action: number;
}

Balancy.Actions.View.setOnMessageReceivedCallback((msg: string): boolean => {
    const messageInfo: MessageExample = JSON.parse(msg);

    if (messageInfo.action === 200) {
        console.log("View is closing!");
        // Perform any cleanup or tracking here
        // Log analytics event
        // Save player preferences
        // Resume game music
    }

    return true; // Continue processing - let Balancy close the view
});

SDK to JavaScript: Broadcasting Messages

Send one-way messages from your game to JavaScript that will be broadcast to all subscribers.

SDK Side: Sending Messages

// Send a custom message to the WebView
Balancy.Actions.View.SendCustomMessageToView(
    "{\"action\":\"greeting\", \"message\":\"Hello from Unity!\"}"
);

// Send game state updates
Balancy.Actions.View.SendCustomMessageToView(
    "{\"action\":\"score-update\", \"score\":" + currentScore + "}"
);

// Trigger animations
Balancy.Actions.View.SendCustomMessageToView(
    "{\"action\":\"play-animation\", \"name\":\"victory\"}"
);
// Send a custom message to the WebView
Balancy.Actions.View.sendCustomMessageToView(
    JSON.stringify({action: "greeting", message: "Hello from TypeScript!"})
);

// Send game state updates
Balancy.Actions.View.sendCustomMessageToView(
    JSON.stringify({action: "score-update", score: currentScore})
);

// Trigger animations
Balancy.Actions.View.sendCustomMessageToView(
    JSON.stringify({action: "play-animation", name: "victory"})
);

JavaScript Side: Receiving Messages

Subscribe to these messages in your view:

// Method 1: Subscribe with callback (recommended)
const unsubscribe = balancy.subscribeToCustomMessages((data) => {
    console.info('Received custom message:', data);

    if (data.action === 'greeting') {
        console.log('Message:', data.message);
        showNotification(data.message);
    }

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

    if (data.action === 'play-animation') {
        playAnimation(data.name);
    }
});

// To unsubscribe later:
unsubscribe();

// Method 2: Listen to DOM event
window.addEventListener('balancy-custom-message', (event) => {
    console.log('Received custom message:', event.detail);
});

Multiple Subscribers

Messages are broadcast to all 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

Key Features:

  • Messages are broadcast to all subscribers
  • Multiple subscribers can listen simultaneously
  • Errors in one subscriber don't affect others
  • Both callback-based and event-based subscription methods available

Complete Custom Message Examples

Example 1: Level Selection

JavaScript (sending request):

document.getElementById('level-3').onclick = async () => {
    const response = await balancy.sendCustomMessage('level_select', {
        levelId: 3,
        difficulty: 'hard'
    });

    if (response.canPlay) {
        balancy.closeView('Starting level');
    } else {
        alert(`Level locked! Unlock at player level ${response.requiredLevel}`);
    }
};

C# (responding):

Balancy.Actions.View.SetOnMessageReceivedCallback((msg) =>
{
    var messageInfo = JsonUtility.FromJson<CustomMessage>(msg);

    if (messageInfo.action == 1000 && messageInfo.sender == "level_select")
    {
        var levelId = messageInfo.params["levelId"];
        var canPlay = PlayerData.CanPlayLevel(levelId);

        var response = new {
            canPlay = canPlay,
            requiredLevel = canPlay ? 0 : 10
        };

        Balancy.Actions.View.SendResponseToView(
            messageInfo.id,
            JsonUtility.ToJson(response)
        );
        return false;
    }

    return true;
});

Example 2: Real-time Score Updates

C# (broadcasting during gameplay):

void OnScoreChanged(int newScore)
{
    if (isShopViewOpen)
    {
        var message = new {
            action = "score-update",
            score = newScore,
            currency = PlayerData.GetCoins()
        };

        Balancy.Actions.View.SendCustomMessageToView(
            JsonUtility.ToJson(message)
        );
    }
}

JavaScript (receiving and updating UI):

balancy.subscribeToCustomMessages((data) => {
    if (data.action === 'score-update') {
        // Animate score change
        animateCounterChange('score', data.score);
        animateCounterChange('coins', data.currency);

        // Check if player can now afford items
        updateButtonStates();
    }
});


Best Practices

1. Always Handle Message Errors

// ✓ Good: Handle both success and failure
balancy.sendCustomMessage('action', data)
    .then(response => handleSuccess(response))
    .catch(error => handleError(error));

// ✗ Avoid: No error handling
const response = await balancy.sendCustomMessage('action', data);

2. Use Action Codes Consistently

Define constants for action codes:

public static class CustomActions
{
    public const int LevelSelect = 1000;
    public const int EquipItem = 1001;
    public const int CraftItem = 1002;
}

if (messageInfo.action == CustomActions.LevelSelect) { ... }
const CustomActions = {
    LevelSelect: 1000,
    EquipItem: 1001,
    CraftItem: 1002
};

balancy.sendCustomMessage('level_select', {
    action: CustomActions.LevelSelect,
    levelId: 3
});

3. Return Appropriate Boolean Values

// ✓ Good: Return false when sending custom response
Balancy.Actions.View.SendResponseToView(messageInfo.id, customResponse);
return false;  // Prevent default response

// ✓ Good: Return true for other messages
if (messageInfo.action == 200) {
    Debug.Log("View closing");
    return true;  // Let Balancy handle it
}

4. Clean Up Subscriptions

// ✓ Good: Unsubscribe when view closes
const unsubscribe = balancy.subscribeToCustomMessages(handler);

window.addEventListener('beforeunload', () => {
    unsubscribe();
});

// Or in component lifecycle
class MyComponent extends balancy.ElementBehaviour {
    awake() {
        this.unsubscribe = balancy.subscribeToCustomMessages(
            (data) => this.handleMessage(data)
        );
    }

    onDestroy() {
        this.unsubscribe();
    }
}

5. Document Your Custom Actions

Maintain a shared document listing all custom actions:

Action 1000: Level Selection
  JS → SDK: { levelId: number, difficulty: string }
  SDK → JS: { canPlay: boolean, requiredLevel: number }

Action 1001: Equip Item
  JS → SDK: { itemId: string, slot: string }
  SDK → JS: { success: boolean, error?: string }

Troubleshooting

Messages Not Being Received

Problem: SDK callback never called.

Solutions:

  • Verify callback was set before view opened
  • Check action code matches (e.g., 1000 vs 1001)
  • Look for JSON parsing errors in logs
  • Ensure message format is correct

Response Not Reaching JavaScript

Problem: Promise never resolves in JavaScript.

Solutions:

  • Verify SendResponseToView() is called with correct message ID
  • Check you're returning false after sending custom response
  • Ensure response is valid JSON
  • Look for errors in browser console

Broadcast Messages Not Received

Problem: subscribeToCustomMessages() callback not firing.

Solutions:

  • Verify subscription happened before message was sent
  • Check sendCustomMessageToView() is being called
  • Ensure JSON format is correct
  • Look for JavaScript errors that might stop execution

View Not Closing

Problem: View stays open after close message.

Solutions:

  • Verify you're returning true for action 200 (don't block it)
  • Check balancy.closeView() is being called in JavaScript
  • Look for JavaScript errors preventing close
  • Remember automatic close button is always available

Next Steps