Docs / Unity SDKSDK downloadsGitHub ↗

Unity SDK

Preview · 0.1.0-beta.3 · Unity 2022.3 LTS / Unity 6 LTS · Android and iOS

Download the Unity UPM archive and keep it inside the project, for example in Packages/vendor/, so teammates and CI resolve it. Then open Window → Package Manager → + → Add package from tarball… and pick it. Unity records it in Packages/manifest.json:

"com.kemegames.pushhub": "file:vendor/pushhub-unity-0.1.0-beta.3.tgz"

The archive contains the Android core. Install from the archive rather than from a Git URL: the Android core AAR is committed to the package only at release commits, so a Git URL at any other commit may carry an older core or none. Earlier archives (0.1.0-beta.2, 0.1.0-beta.1) stay available; they contain only C#.

Feature Unity package Android core iOS SDK
Install, identity, consent, events 0.1.0-beta.1 0.1.0-beta.2 0.1.0-beta.1
Device context (reported by the native SDK) any 0.1.0-beta.5 next iOS release
Remote config (PushHub.Config) 0.1.0-beta.2 0.1.0-beta.5 defaults only (iOS SDK 0.1.0-beta.2 pending)
Instant refresh of remote config 0.1.0-beta.2 0.1.0-beta.6 later Unity release (iOS SDK 0.1.0-beta.2 pending)
Android core and its dependencies inside the package 0.1.0-beta.3 0.1.0-beta.7 (bundled) not applicable

Remote config works on Android now. On iOS, Unity 0.1.0-beta.2 calls only APIs in the published iOS SDK 0.1.0-beta.1, so remote config returns your in-app defaults: every fetch fails with a warning and returns false. iOS support arrives in a later Unity release that pairs with iOS SDK 0.1.0-beta.2, which is pending a macOS build.

PushHub.Configure("YOUR_APP_ID", "ph_pub_YOUR_KEY");
await PushHub.RequestPermissionAsync();
await PushHub.StartAsync();
await PushHub.IdentifyAsync("player-1842", new Dictionary<string,string> { ["tier"] = "gold" });
await PushHub.TrackAsync("level_completed", new Dictionary<string,string> { ["level"] = "20" });

The C# facade selects an Android JNI or iOS native bridge at build time. Secure storage, offline retry, FCM/APNs token lifecycle, consent, and signed receipts remain in the native core. Editor play mode uses a non-networking preview adapter. Inspect configuration under Window → PushHub → Diagnostics.

The SDK does not prompt during Configure, navigate automatically, or accept ph_live_ server keys. Forward allowed deep links through PushHub.DeepLinkOpened and validate them in the game.

Preview status means the source contract and Android bridge compile, but the advertised Unity editor/device build matrix remains a Phase 4E release gate.

Android setup

From package 0.1.0-beta.3 the package carries its Android core, so you don't copy an AAR or edit Gradle templates:

  • Runtime/Plugins/Android/pushhub-android-0.1.0-beta.7.aar is the Android core, the same file as com.kemegames.pushhub:pushhub-android:0.1.0-beta.7 in PushHub's Maven repository. It is enabled for Android only.
  • Runtime/Plugins/Android/PushHub.androidlib is an Android Library plug-in that declares the core's dependencies (Firebase Messaging, AndroidX Core, Security Crypto, WorkManager, Kotlin coroutines and the Kotlin standard library). They come from google() and mavenCentral(), which every Unity Android build already uses. Unity's generated settings.gradle ignores repositories declared by plug-ins, so the package doesn't rely on one.

Upgrading from 0.1.0-beta.2 or earlier: remove the pushhub-android-*.aar you copied to Assets/Plugins/Android/ and the PushHub lines you added to mainTemplate.gradle. Otherwise the build fails with duplicate classes.

What you still do yourself:

  1. Add your Firebase values (from google-services.json) as Android string resources: google_app_id, gcm_defaultSenderId, google_api_key, project_id and google_storage_bucket. Or use the Firebase Unity SDK, which generates them.
  2. Player Settings: Minimum API Level 23 or higher, Target API Level Automatic (with Android platform 35 or later installed, because AndroidX Core 1.15 needs it), IL2CPP, and ARM64. Set Managed Stripping Level to Minimal, or keep Keme.PushHub in Assets/link.xml.
  3. The core's AndroidX dependencies need android.useAndroidX=true. If the build stops because that property isn't enabled, turn on Custom Gradle Properties Template and add the line to gradleTemplate.properties. This hasn't been checked in the editor yet.
  4. Don't add pushhub.json as an Android asset. The package configures PushHub from C#, and a bundled config makes PushHub.Configure fail with "already configured".

Verification so far: the package contents, the AAR against the Maven repository, and a Gradle build with the shape of Unity's Android export (repositories only in settings.gradle with PREFER_SETTINGS, the AAR in unityLibrary/libs, PushHub.androidlib as a child project) are checked by scripts/sdk-consumer-validation/unity.mjs. No Unity editor build or device run has happened, so Unity stays Preview.

Not yet verified on a device:

  • StartAsync blocks the main thread while it registers, so call it on a loading screen.
  • TrackAsync on Android: packages up to 0.1.0-beta.2 fail with NoSuchMethodError, because the Android core's track bridge method returned a value where CallStatic expects none. Android core 0.1.0-beta.7, which package 0.1.0-beta.3 ships, fixes the bridge. The fix is unit-tested and checked against the AAR's bytecode, but no Unity build has run it yet.
  • On iOS, the package doesn't register devices.

See SDK support levels for the full list.

Remote config

Requires Unity package 0.1.0-beta.2 or later. Android needs the Android core 0.1.0-beta.5 or later and works now. On iOS the getters return your in-app defaults until a later Unity release pairs with iOS SDK 0.1.0-beta.2 (pending a macOS build).

Remote config lets you change values in a shipped game, such as starter coins, a maintenance flag or a store layout, from the PushHub dashboard. You reach it through PushHub.Config.

Setup and defaults

Set in-app defaults before you read any value. The getters use them before the first fetch and for any key the server leaves out.

PushHub.Config.SetDefaults(new Dictionary<string, object>
{
    ["starter_coins"] = 100,
    ["maintenance_mode"] = false,
    ["welcome_text"] = "Welcome!",
    ["store_layout"] = "{\"tabs\":[\"offers\",\"coins\"]}"   // a JSON string, or a Dictionary/List
});

SetDefaults replaces the previous defaults, and a null value is ignored.

Fetch on load

PushHub.Configure("YOUR_APP_ID", "ph_pub_YOUR_KEY");
await PushHub.StartAsync();
bool changed = await PushHub.Config.FetchAndActivateAsync();   // true when the active values changed

Fetching needs a registered installation, so call it after StartAsync. On Android, the native SDK reports a changed device context (for example a new app version) before it fetches, so targeting conditions use current values.

To apply config only at a safe moment, such as a loading screen, fetch and activate separately:

await PushHub.Config.FetchAsync();      // downloads, but the getters don't change yet
// ...later, on the loading screen
if (PushHub.Config.Activate()) ApplyConfig();

Read values

long coins = PushHub.Config.GetLong("starter_coins");
bool maintenance = PushHub.Config.GetBool("maintenance_mode");
double multiplier = PushHub.Config.GetDouble("price_multiplier");
string text = PushHub.Config.GetString("welcome_text");
string layoutJson = PushHub.Config.GetJson("store_layout");          // raw JSON text, or null
var layout = JsonUtility.FromJson<StoreLayout>(layoutJson);
PushHubRemoteConfigSource source = PushHub.Config.GetValue("starter_coins").Source;   // Remote, Default or Static
IReadOnlyDictionary<string, PushHubRemoteConfigValue> all = PushHub.Config.GetAll();

Each getter looks for the active remote value first, then your in-app default, and otherwise returns the type's zero value ("", false, 0, or null for JSON). The getters never throw, and they read a cached snapshot, so they are cheap to call every frame.

Listen for updates

using System.Linq;

PushHub.Config.OnUpdated += keys =>
{
    if (keys.Contains("maintenance_mode")) RefreshMaintenanceBanner();
};

OnUpdated runs on the main thread after an activation that changed values. It receives only the keys that were added, removed or changed.

Offline behaviour

The native SDK stores the active values, ETag, version and last fetch time on the device. After Configure, the last activated config is available offline and on the next cold start, before any network call.

A failed fetch never throws: FetchAndActivateAsync and FetchAsync return false, log a warning, and keep the active values. Each fetch sends the stored ETag, so an unchanged config costs a 304 Not Modified.

Fetch interval

A fetch reaches the network at most once per interval. The interval is the larger of the server's minimumFetchIntervalSeconds (900 seconds by default) and PushHub.Config.MinimumFetchIntervalSeconds. Inside the interval, a fetch succeeds and uses the stored config. Pass force: true to skip the interval in development builds. Each fetch times out after 10 seconds.

PushHub.Config.MinimumFetchIntervalSeconds = 3600;
await PushHub.Config.FetchAndActivateAsync(force: Debug.isDebugBuild);

Instant refresh

When you publish, roll back or schedule a publish with Notify devices set to Test devices only or All players, PushHub sends the environment's devices a silent message, and the native SDK fetches and activates the newer config.

  • Android: with Android core 0.1.0-beta.6 or later, refreshes are applied by the native SDK while the game is running; no code is needed. Only refreshes that arrive while the game is running are supported. The Android core 0.1.0-beta.7 in package 0.1.0-beta.3 can configure itself from the values the game last passed to PushHub.Configure when Android starts the process for a refresh, but that hasn't been validated with Unity, so don't rely on it. If another Firebase messaging plug-in is installed, the refresh may go to it instead. See SDK support levels.
  • iOS: arrives with the Unity release that pairs with the next iOS SDK, like remote config itself. Then turn on Background Modes → Remote notifications, either in Xcode or under Player Settings → iOS → Other Settings with Enable Custom Background Behaviors. Unity's app controller answers background notifications itself, so add this file as Assets/Plugins/iOS/PushHubAppController.mm to hand PushHub's to the SDK first:
#import "UnityAppController.h"

extern "C" BOOL PushHubHandleBackgroundNotification(NSDictionary *userInfo, void (^completionHandler)(UIBackgroundFetchResult));

@interface PushHubAppController : UnityAppController
@end

@implementation PushHubAppController
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
    fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    if (PushHubHandleBackgroundNotification(userInfo, completionHandler)) return;
    [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
@end

IMPL_APP_CONTROLLER_SUBCLASS(PushHubAppController)

A project can have only one IMPL_APP_CONTROLLER_SUBCLASS. If yours already has one, add the method to that controller instead.

The native SDK activates the new values right away. PushHub.Config shows them, and raises OnUpdated, the next time the game calls FetchAndActivateAsync or Activate (a call inside the minimum interval makes no request but still picks them up), or at the next launch. Instant refresh is best effort, so keep fetching on load.

Signed config is not available in this SDK. It has no API to set trusted signing keys, so it accepts unsigned config even when Config signing is turned on for the environment. Native verification exists in the Android core only when your own Android code sets the keys. See SDK support levels.

Platforms

  • Android: uses the native Android SDK's config API.
  • iOS: defaults only in 0.1.0-beta.2. Every fetch fails with the warning "Remote config on iOS requires PushHub iOS SDK 0.1.0-beta.2 (pending release)", returns false, and the getters return your in-app defaults. The later Unity release that pairs with that SDK uses its PushHub.config, whose fetch always activates natively, so after FetchAsync the native SDK already stores the new values. The C# getters still show the previous values until you call Activate. A cold start loads the newest values. The game's minimum interval applies within the session, and the server's interval always applies.
  • Editor and other platforms: no network calls. The getters return your in-app defaults, the fetch methods return false, and the SDK logs this once.