# Unity

The **OnsideKit Unity** package lets you use OnsideKit from C# in a Unity game that ships to iOS. It is a thin wrapper over the native OnsideKit iOS SDK: your C# calls are forwarded to the native SDK, and native events are delivered back to C#. The same concepts apply — products, the payment queue, transactions, login, attribution, and event tracking — so the [Core Concepts](/sdk/core-concepts/payment-queue.md) and [Error Reference](/sdk/reference/errors.md) pages are good companion reading.

* **Package:** `io.onside.onsidekit-unity`
* **Namespace:** `OnsideKit`
* **Requires:** Unity 2022.1+, an iOS build target

{% hint style="info" %}
The native bridge exists only in an **iOS device/simulator build**. In the Unity Editor (and on non-iOS platforms) the API calls are no-ops, so you test the actual purchase flow by building and running on iOS.
{% endhint %}

## 1. Install the package

Download `OnsideKit-Unity-<version>.unitypackage` from the [Releases of the OnsideKit-iOS repository](https://github.com/onside-io/OnsideKit-iOS/releases). In Unity, choose **Assets → Import Package → Custom Package**, select the file, and click **Import All**.

The External Dependency Manager (EDM4U) is bundled in the package, and the native OnsideKit SDK is pulled in automatically when you build for iOS — you don't need to add any scoped registries or extra dependencies.

## 2. Create the settings asset

In Unity: **Assets → Create → OnsideKit → Settings**.

This creates `Assets/OnsideKit/Resources/OnsideKitSettings.asset`. Set:

* **Callback Scheme** *(required)* — your app's unique URL scheme (e.g. `myapp-onside`). It must match the scheme you pass to `Onside.Initialize(...)`, and the **iOS build fails if it's empty**. The Onside Store app uses it to return to your game after login/payment.
* **StoreKit Configuration Path** *(optional)* — path to a `.storekit` file for offline [local testing](/sdk/advanced-and-tooling/local-testing.md). The file is copied into the generated Xcode project.

## 3. Write your integration

```csharp
using System.Collections.Generic;
using OnsideKit;
using UnityEngine;

public class StoreManager : MonoBehaviour, IOnsidePaymentTransactionObserver
{
    void Start()
    {
        // 1. Initialize once. Use the same scheme as in OnsideKitSettings.
        Onside.Initialize("myapp-onside");

        // 2. Subscribe to product events and register as a transaction observer.
        var queue = Onside.DefaultPaymentQueue;
        queue.ProductsReceived += OnProductsReceived;
        queue.ProductsRequestFailed += error => Debug.LogError($"Products failed: {error}");
        // The only signal that an Add(payment) was rejected before a transaction
        // was created (e.g. the user dismissed login).
        queue.AddPaymentFailed += (productId, error) =>
            Debug.LogWarning($"Purchase rejected: {productId} — {error}");
        queue.Add(this);

        // 3. Fetch products.
        queue.RequestProducts(new List<string> { "premium_feature", "remove_ads" });
    }

    void OnProductsReceived(IList<OnsideProduct> products, IList<string> invalidIdentifiers)
    {
        foreach (var p in products)
            Debug.Log($"{p.localizedTitle} — {p.price.value} {p.price.currencyCode}");
    }

    // Start a purchase (e.g. from a Buy button).
    public void Buy(string productIdentifier)
    {
        Onside.DefaultPaymentQueue.Add(new OnsidePayment(productIdentifier));
    }

    // --- IOnsidePaymentTransactionObserver ---

    public void OnsidePaymentQueueUpdatedTransactions(
        OnsidePaymentQueue queue, IList<OnsidePaymentTransaction> transactions)
    {
        foreach (var t in transactions)
        {
            switch (t.State)
            {
                case OnsidePaymentTransactionState.Purchased:
                case OnsidePaymentTransactionState.Restored:
                    UnlockContent(t.productIdentifier);
                    queue.FinishTransaction(t);   // required
                    break;
                case OnsidePaymentTransactionState.Failed:
                    Debug.LogWarning($"Purchase failed: {t.Error}");
                    queue.FinishTransaction(t);   // required
                    break;
                case OnsidePaymentTransactionState.Purchasing:
                    break;   // in progress — wait
            }
        }
    }

    void UnlockContent(string productIdentifier) { /* grant + persist */ }

    public void OnsidePaymentQueueRemovedTransactions(
        OnsidePaymentQueue queue, IList<OnsidePaymentTransaction> transactions) { }
    public void OnsidePaymentQueueRestoreCompletedTransactionsFinished(
        OnsidePaymentQueue queue) { }
    public void OnsidePaymentQueueRestoreCompletedTransactionsFailed(
        OnsidePaymentQueue queue, OnsideTransactionsRestoreError error) { }
    public void OnsidePaymentQueueDidChangeStorefront(
        OnsidePaymentQueue queue) { }
}
```

### Key rules

{% hint style="warning" %}

* **Callbacks run on Unity's main thread** — you can safely touch Unity APIs inside them.
* **Finish every transaction** — call `FinishTransaction` for each `Purchased`, `Restored`, or `Failed` transaction, or it is re-delivered on the next launch.
* **`AddPaymentFailed` is the only signal** that an `Add(payment)` was rejected before a transaction was created (e.g. the user dismissed login). The transaction observer is **not** called in that case.
* **The Editor is a no-op** — methods and callbacks run only in an on-device iOS build (outside it you get a warning and callbacks don't fire). Test on a device/simulator.
* **`ShouldContinueHandler` must return quickly** — the native purchase flow blocks waiting for its answer.
  {% endhint %}

## 4. Build for iOS

1. **File → Build Settings → iOS → Switch Platform**, then **Build**.
2. A post-process build step runs automatically and configures the generated Xcode project: it embeds `OnsideKit.framework`, adds `onside` to `LSApplicationQueriesSchemes`, registers your callback scheme under `CFBundleURLTypes`, copies your `.storekit` file (if set), and applies the required Swift/build settings.
3. Open the generated `.xcodeproj` and run on a device (or the simulator).

You don't need to edit the Xcode project or `Info.plist` by hand, and you don't implement any URL handling yourself — incoming URLs are caught automatically by the bundled app controller.

## How it works

Your C# calls are marshaled to the native SDK; native events are delivered back to a persistent `OnsideKit` GameObject (created by `Initialize`) via `UnitySendMessage` and surfaced as C# events/observer callbacks.

```
Onside.Initialize("myapp-onside")
  → creates the persistent "OnsideKit" GameObject
  → native [Onside initialize] + callbackScheme + delegate/observer wiring

DefaultPaymentQueue.RequestProducts(ids)
  → native OnsideProductsRequest → products serialized to JSON
  → C# OnsidePaymentQueue.ProductsReceived fires

DefaultPaymentQueue.Add(payment)
  → native purchase flow (may open the Onside Store app)
  → Store app returns via myapp-onside:// → native handleURL
  → transaction updates → IOnsidePaymentTransactionObserver
```

***

## API Reference

### `Onside` (static)

```csharp
static void Initialize(string callbackScheme, string storeKitConfigurationName = null)
```

Initializes the SDK and creates the persistent `OnsideKit` GameObject. Call once at startup, before any other call. Idempotent — a second call is ignored. `storeKitConfigurationName` enables offline [local testing](/sdk/advanced-and-tooling/local-testing.md).

<table><thead><tr><th width="430">Member</th><th>Description</th></tr></thead><tbody><tr><td><code>OnsidePaymentQueue DefaultPaymentQueue { get; }</code></td><td>The shared payment queue.</td></tr><tr><td><code>void RequestLogin(Action onSuccess = null, Action&#x3C;OnsideLoginError> onFailed = null)</code></td><td>Present the login flow. Login is also triggered on demand by a purchase or by presenting the payment methods manager.</td></tr><tr><td><code>void Logout()</code></td><td>End the current session.</td></tr><tr><td><code>void SetThemeMode(OnsideUIThemeMode mode)</code></td><td>Global theme: <code>Auto</code>, <code>Light</code>, or <code>Dark</code>.</td></tr><tr><td><code>void SetDelegateOptions(string countryCodeHint = null, bool forceLocalLoginMethods = false)</code></td><td>Pre-login region hint and force the in-app login screen. See <a href="/pages/5oGGH6dyxvl0lcEMuSqx">The Onside Delegate</a>.</td></tr><tr><td><code>void SetApplePayMerchantIdentifier(string merchantIdentifier)</code></td><td>Configure Apple Pay. No-op if the SDK build has no Apple Pay. See <a href="/pages/d68KNbPlPOh4ZwOy2bDc">Apple Pay</a>.</td></tr><tr><td><code>void PresentPaymentMethodsManager(Action onSuccess = null, Action&#x3C;OnsidePaymentMethodsManagerError> onFailed = null)</code></td><td>Show the saved-cards manager.</td></tr><tr><td><code>void GetAttributionMetadata(Action&#x3C;OnsideAttributionMetadata> onSuccess = null, Action&#x3C;OnsideAttributionMetadataError> onFailed = null)</code></td><td>Fetch <a href="/pages/CTU1ZKPvzowOnIDPO1rq">attribution</a> metadata.</td></tr><tr><td><code>void Track(string eventName, Dictionary&#x3C;string, object> parameters = null)</code></td><td>Send an analytics event. See <a href="/pages/hwXMpbVJlmfOrR46RUTt">Event tracking</a>.</td></tr><tr><td><code>void ResetLocalTestingState()</code></td><td>Reset local-testing purchase history.</td></tr></tbody></table>

**Signed in-app purchase history** — subscribe to the static events, then request:

```csharp
static event Action<OnsideSignedInAppsHistory> SignedInAppsHistoryReceived;
static event Action<OnsideSignedInAppsHistoryRequestError> SignedInAppsHistoryFailed;
static event Action SignedInAppsHistoryFinished;

static void RequestSignedInAppsHistory();
static void StopSignedInAppsHistoryRequest();
```

See [Signed In-App Purchase History](/sdk/purchase-validation/signed-in-apps-history.md).

**Installation id** — for support/diagnostics (see [Debugging](/sdk/advanced-and-tooling/debugging.md)):

```csharp
static string InstallationId { get; }
static event Action<string> InstallationIdChanged;   // may deliver null while loading
static void SubscribeInstallationId();               // call once to start receiving
```

### `OnsidePaymentQueue`

Accessed via `Onside.DefaultPaymentQueue`.

```csharp
OnsideStorefront Storefront { get; }                  // null until logged in
IList<OnsidePaymentTransaction> Transactions { get; } // snapshot of current transactions

event Action<IList<OnsideProduct>, IList<string>> ProductsReceived;  // (products, invalidIdentifiers)
event Action<OnsideProductsRequestError> ProductsRequestFailed;
event Action ProductsRequestFinished;
event Action<string, OnsidePaymentQueueAddProductError> AddPaymentFailed;  // (productIdentifier, error)

// Optional storefront/price-change gate. Return false to cancel a transaction
// whose storefront changed. Keep it fast; if null, transactions always continue.
Func<OnsidePaymentTransaction, OnsideStorefront, bool> ShouldContinueHandler;

void RequestProducts(IList<string> productIdentifiers);
void Add(OnsidePayment payment);
void FinishTransaction(OnsidePaymentTransaction transaction);
void RestoreCompletedTransactions(Action<bool> completion = null);

void Add(IOnsidePaymentTransactionObserver observer);     // replays current transactions
void Remove(IOnsidePaymentTransactionObserver observer);
```

The `ShouldContinueHandler` is the Unity equivalent of the native storefront safety gate — see [Handling Storefront & Price Changes](/sdk/purchasing/storefront-price-changes.md).

### `IOnsidePaymentTransactionObserver`

<table><thead><tr><th width="430">Method</th><th>When</th></tr></thead><tbody><tr><td><code>OnsidePaymentQueueUpdatedTransactions(queue, transactions)</code></td><td>Transactions added or changed state. Finish each completed one.</td></tr><tr><td><code>OnsidePaymentQueueRemovedTransactions(queue, transactions)</code></td><td>Transactions removed (after finishing).</td></tr><tr><td><code>OnsidePaymentQueueRestoreCompletedTransactionsFinished(queue)</code></td><td>A restore finished successfully.</td></tr><tr><td><code>OnsidePaymentQueueRestoreCompletedTransactionsFailed(queue, error)</code></td><td>A restore failed.</td></tr><tr><td><code>OnsidePaymentQueueDidChangeStorefront(queue)</code></td><td>The storefront changed (login/logout/region).</td></tr></tbody></table>

### Types

```csharp
class OnsidePayment {
    string productIdentifier { get; }
    string appAccountToken { get; }   // optional, ties the purchase to your account
    OnsidePayment(string productIdentifier, string appAccountToken = null);
}

enum OnsidePaymentTransactionState { Unknown = -1, Purchasing = 0, Purchased = 1, Restored = 2, Failed = 3 }

class OnsidePaymentTransaction {
    string id;
    string transactionIdentifier;
    string originalTransactionIdentifier;
    string productIdentifier;
    string appAccountToken;
    OnsideStorefront storefront;
    OnsidePaymentTransactionState State { get; }
    OnsidePaymentTransactionError? Error { get; }   // set only when Failed
}

class OnsideProduct {
    string productIdentifier;
    string localizedTitle;
    string localizedDescription;
    OnsidePrice price;          // value (double) + currencyCode (string)
    string iconUrl;
    string subscriptionGroupIdentifier;   // subscriptions only; null otherwise
    OnsidePeriod subscriptionPeriod;      // subscriptions only; null for one-time products
}

class OnsidePeriod { string unit; long value; }   // unit: "day" | "week" | "month" | "year"

class OnsideStorefront { string id; string countryCode; }

class OnsideAttributionMetadata { string refererUrl; }

class OnsideSignedInAppsHistory {
    string Text { get; }    // decoded UTF-8 payload
    byte[] Bytes { get; }   // decoded raw bytes
}

enum OnsideUIThemeMode { Auto = 0, Light = 1, Dark = 2 }
```

The subscription fields on `OnsideProduct` (the billing period and group) carry the same meaning as in the native SDK — see [Subscriptions](/sdk/products-and-subscriptions/subscriptions.md).

### Errors

All error enums include an `Unknown` fallback. Notable cases:

<table><thead><tr><th width="360">Enum</th><th>Cases</th></tr></thead><tbody><tr><td><code>OnsideProductsRequestError</code></td><td><code>Cancelled</code>, <code>ConnectionError</code>, <code>AppNotRegistered</code>, <code>InvalidProductIdentifier</code>, <code>ServiceUnavailable</code>, <code>InternalError</code></td></tr><tr><td><code>OnsidePaymentQueueAddProductError</code></td><td><code>LoginDiscarded</code></td></tr><tr><td><code>OnsideTransactionsRestoreError</code></td><td><code>Cancelled</code>, <code>ConnectionError</code>, <code>AppNotRegistered</code>, <code>ServiceUnavailable</code>, <code>InternalError</code>, <code>LoginDiscarded</code></td></tr><tr><td><code>OnsidePaymentTransactionError</code></td><td><code>Cancelled</code></td></tr><tr><td><code>OnsideLoginError</code></td><td><code>LoginDiscarded</code>, <code>RequestAlreadyInProgress</code></td></tr><tr><td><code>OnsidePaymentMethodsManagerError</code></td><td><code>LoginDiscarded</code>, <code>NotSupportedInLocalTesting</code>, <code>RequestAlreadyInProgress</code></td></tr><tr><td><code>OnsideAttributionMetadataError</code></td><td><code>ConnectionError</code>, <code>AppNotRegistered</code>, <code>ServiceUnavailable</code>, <code>InternalError</code>, <code>RequestAlreadyInProgress</code></td></tr><tr><td><code>OnsideSignedInAppsHistoryRequestError</code></td><td><code>NotLoggedIn</code>, <code>NotSupportedInLocalTesting</code>, <code>Cancelled</code>, <code>ConnectionError</code>, <code>AppNotRegistered</code>, <code>ServiceUnavailable</code>, <code>InternalError</code></td></tr></tbody></table>

The meaning of each case matches the native SDK — see the [Error Reference](/sdk/reference/errors.md). The `RequestAlreadyInProgress` cases are Unity-specific: they're returned when you start a one-shot request (login, payment methods, attribution) while a previous one is still pending.

## Common issues

<table><thead><tr><th width="320">Issue</th><th>Fix</th></tr></thead><tbody><tr><td>Callback scheme doesn't work</td><td>Ensure the <strong>Callback Scheme</strong> in <code>OnsideKitSettings</code> matches the string passed to <code>Onside.Initialize()</code>.</td></tr><tr><td>Products request returns nothing</td><td>Check that your product identifiers match what's configured in the Onside Developer Console.</td></tr><tr><td>Nothing happens in the Editor</td><td>Expected — the native bridge only runs in an iOS build. Test on a device/simulator.</td></tr></tbody></table>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.onside.io/sdk/integrations/unity.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
