Yes2SDK for Unity
A single SDK for your Unity WebGL game. Integrate once against Yes2SDK, submit through the Yes2Games Dashboard, and the Yes2Games team handles the rest.
Supported platforms: Poki, CrazyGames, Yandex Games, GameDistribution, YouTube Playables and Jest.
The current SDK version is also exposed at runtime via Yes2SDK.Version (string), so you can log it for support tickets.
Requirements
- Unity 2021.3 or newer (Unity 6 supported — see Building)
- WebGL build target
com.unity.nuget.newtonsoft-json(>= 3.2.1)
Installation
Via Git URL (recommended)
- Open Window > Package Manager
- Click + > Add package from git URL...
- Enter:
https://github.com/yes2games/yes2sdk-unity.git#v2.12.0 - Click Add
Pinning the URL with the release tag keeps the package hash stable across resolves. Bump the tag when a newer release ships. Without a tag, Package Manager re-resolves against
mainon every refresh and reports phantom diffs.
Channels
integration: https://github.com/yes2games/yes2sdk-unity.git#edge
production: https://github.com/yes2games/yes2sdk-unity.git#vX.Y.Z
edge is a mutable Git tag that moves to the newest commit on main whose required CI went green, and it is a tag and nothing else — there is no GitHub Release and no prerelease behind it. Use it to integrate against unreleased work, never to ship. vX.Y.Z is a tag and a GitHub Release at the exact commit that was tested, and it is never rewritten — a content fix ships as a new SemVer rather than as a moved tag. That is the only thing a shipping game should resolve.
Via Local Folder
- Clone or download the repository
- Open Window > Package Manager
- Click + > Add package from disk...
- Navigate to the
Unity/folder and selectpackage.json
Initial Setup
- Open Yes2SDK > Build Window in the Unity menu bar
- Click Install Template — this installs the
Yes2SDK-SuperSDKWebGL template into your project - The status indicator changes from "Setup Pending" to "Ready"
After updating the SDK package, click Reinstall Template in the Build Window to copy changes into your project.
The package's EditMode tests stay hidden from the Test Runner until the project opts in. See Automated tests for the one-line
testablesentry.
Quick Start
This is the minimum integration your game must have. The lifecycle has three distinct stages — don't chain them together:
App launch → InitializeAsync (SDK ready)
Splash + loading → SetLoadingProgress(0..100) as assets load
Game playable → StartGameAsync (scene ready, accepting input)
using System.Collections;
using UnityEngine;
using Yes2SDK;
public class GameManager : MonoBehaviour
{
void Start()
{
// Stage 1 — at app launch, initialize the SDK.
Yes2SDK.InitializeAsync(
onSuccess: () =>
{
Debug.Log("SDK ready");
StartCoroutine(LoadAndStart());
},
onError: err => Debug.LogError(err)
);
}
IEnumerator LoadAndStart()
{
// Stage 2 — load your assets and report progress (0..100).
for (int p = 0; p <= 100; p += 10)
{
Yes2SDK.SetLoadingProgress(p);
yield return new WaitForSeconds(0.05f); // replace with real loading work
}
// Stage 3 — splash done, scene loaded, game is playable.
Yes2SDK.StartGameAsync(
onSuccess: () => Debug.Log("Game started"),
onError: err => Debug.LogError(err)
);
}
}
Don't call
StartGameAsyncdirectly inside theInitializeAsyncsuccess handler. The platform's loading bar treatsStartGameAsyncas "the game is playable now" — calling it before the player has anything to interact with shows a misleading 100% loading state.
Without this flow your game won't be accepted for review.
Core API
Implement everything in this section. Together these cover what Yes2Games needs to validate and monetize your game.
Lifecycle (required)
// Call once at startup
Yes2SDK.InitializeAsync(onSuccess, onError);
// Call as your game loads (0-100)
Yes2SDK.SetLoadingProgress(progress);
// Call when loading finishes and the game is playable
Yes2SDK.StartGameAsync(onSuccess, onError);
Important: these three calls fire in three distinct stages — don't chain them.
InitializeAsyncis at app launch.SetLoadingProgressis updated as your assets load.StartGameAsyncruns only when the game is actually playable (splash gone, scene loaded, accepting input). See Quick Start for the full pattern.
await-friendly overloads
Every *Async method has a Task-returning overload that takes a CancellationToken. Pass CancellationToken.None for no timeout, or a real token for cancellation/timeout support. Errors are thrown as Yes2SDKException (whose ErrorCode matches the Error.ErrorCode of the underlying failure):
using System.Threading;
using System.Threading.Tasks;
using Yes2SDK;
// Cancel init if it hangs for more than 10 seconds
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
await Yes2SDK.InitializeAsync(cts.Token);
Debug.Log("SDK ready");
}
catch (Yes2SDKException ex) when (ex.ErrorCode == ErrorCode.NetworkError)
{
// Platform unreachable — fall back or retry with backoff
}
catch (TaskCanceledException)
{
// Init didn't complete inside the timeout
}
The callback-based form is unchanged and still preferred for short-lived game-loop call sites where allocating Tasks is overkill.
Handle pause / resume so your game reacts when the SDK pauses you (e.g. during an ad):
Yes2SDK.OnPause += () => { Time.timeScale = 0; AudioListener.pause = true; };
Yes2SDK.OnResume += () => { Time.timeScale = 1; AudioListener.pause = false; };
Save progress when the platform closes the game. OnExitRequested is raised only on platforms that report an exit, and the SDK flushes player data right after the handler returns:
Yes2SDK.OnExitRequested += () =>
{
// Synchronous writes only.
Yes2SDK.Data.SetString("save", SerializeProgress());
Yes2SDK.Data.SetInt("level", currentLevel);
};
Async work started in the handler (
SetStringAsync,FlushAsync, a coroutine) is not awaited and may never finish.PlayerPrefs.Save()is not a substitute either: write throughYes2SDK.Dataso the flush picks it up.
Ads (required)
Interstitial ads run at natural break points. Rewarded ads run only when the player opts in.
Always wrap ad calls in
Game.GameplayStop()/Game.GameplayStart(). Platforms count "active gameplay seconds" for monetization — leaving gameplay running during an ad inflates those numbers and is grounds for rejection. See Gameplay Tracking for the full rule.
Yes2SDK.Game.GameplayStop(); // before the ad
Yes2SDK.Ads.ShowInterstitial(
placement: "level-end",
description: "Between levels",
beforeAd: () => PauseGame(),
afterAd: () => { ResumeGame(); Yes2SDK.Game.GameplayStart(); },
onError: err => { ResumeGame(); Yes2SDK.Game.GameplayStart(); }
);
Yes2SDK.Game.GameplayStop(); // before the ad
Yes2SDK.Ads.ShowRewarded(
placement: "extra-life",
description: "Extra life reward",
beforeAd: () => PauseGame(),
afterAd: () => { ResumeGame(); Yes2SDK.Game.GameplayStart(); },
adDismissed: () => { /* no reward — see firing order below */ },
adViewed: () => GiveReward(),
onError: err => { ResumeGame(); Yes2SDK.Game.GameplayStart(); }
);
Resume in
onErroras well asafterAd. This is required, not optional. An ad that fails (no fill, blocked, timed out) ends withonErrorand nothing else. Some platforms still sendafterAdafter a no-fill, but the SDK drops it, because the ad already ended atonError. A game that resumes only inafterAdstays paused after every failed ad. Each ad ends with exactly one ofafterAdoronError, so resuming in both never runs twice.
Rewarded ad firing order
The callbacks fire in this order. Pay attention — getting it wrong silently breaks reward logic:
beforeAd → pause game (always)
(ad shown)
adViewed → grant reward (ONLY fires if the player watched the full ad)
— or —
adDismissed → no reward (fires if the player skipped/closed early)
afterAd → resume game (fires whether the player watched or dismissed)
If the ad fails instead, only onError fires, with no afterAd after it (see above).
Exactly one outcome per ad. Every rewarded ad reports exactly one of adViewed, adDismissed or onError, whatever the platform sends. If the platform completes the ad without reporting whether it was watched, the binding reports it as adDismissed (no reward) just before afterAd and logs a warning. It never reports such an ad as adViewed. A second outcome for the same ad is dropped with a warning, so a dismissed ad never also grants a reward. You can settle your reward flow on the outcome callback alone.
⚠️ Do NOT grant rewards in
afterAd.afterAdfires for both completion and dismissal — granting rewards there gives them away on skip. Always grant inadViewed.
afterAdis last, and it is what completes the ad: the outcome arrives first, thenafterAd. A new ad can be started fromafterAd, but not fromadViewedoradDismissed, because the previous ad is still in flight there.
Format support
Ads.IsInterstitialSupported()/Ads.IsRewardedSupported()— whether the platform serves that ad format at all. Use these to feature-gate ad UI up front instead of hard-coding which platforms have which format. Both reportfalsewhen the injected runtime is older than the checks, so a gate never sends the game into a call the platform cannot serve. In the Editor both reporttrue, because the mock serves either format.- Banner support is a separate check on its own module:
Banners.IsSupported().
// Hide the whole offer where the format is absent, rather than failing on click.
doubleCoinsButton.gameObject.SetActive(Yes2SDK.Ads.IsRewardedSupported());
Support is not readiness: these ask whether the format exists on the platform, IsRewardedAdAvailable() below asks whether an ad looks ready right now.
Concurrent ad guard + readiness
Ads.IsAdShowing()— returnstruewhile aShowInterstitialorShowRewardedis in flight (between the call andafterAd/error). CallingShow*again while one is already showing is rejected immediately —onErrorfires withErrorCode.InvalidParamsand a message starting"Another ad is already in flight (AdAlreadyShowing)…".- An ad that never completes is released automatically. If the platform does not start the ad within 30 seconds, or starts it and does not finish within 3 minutes of play (time in a hidden tab does not count), the ad is released and
onErrorfires withErrorCode.Timeout.IsAdShowing()then returnsfalseand the nextShow*runs normally. A late callback from the released ad is ignored. Resume your game inonErroras well asafterAd, so a timed-out ad does not leave it paused. The Editor mock popup has no timeout. Ads.IsRewardedAdAvailable()— best-effort check whether a rewarded ad appears available right now. Most platform SDKs don't expose explicit readiness, so this returnstrueas long as the platform's ad module is loaded; the actualShowRewardedcall can still fail withnoFill. Use it as a hint, not a guarantee.
rewardButton.interactable =
!Yes2SDK.Ads.IsAdShowing() && Yes2SDK.Ads.IsRewardedAdAvailable();
Gameplay Tracking (required)
Tells Yes2Games when an active round begins and ends. Also call GameplayStop() before any ad and GameplayStart() after.
Yes2SDK.Game.GameplayStart();
Yes2SDK.Game.GameplayStop();
Analytics.LogLevelStart/LogLevelEndalso mark gameplay start / stop. Calling both pairs is safe: repeated starts or stops are ignored, so the platform sees one signal per transition.
Data (required)
Key-value storage. Persists across sessions automatically.
Yes2SDK.Data.SetInt("highScore", 1500);
Yes2SDK.Data.SetString("playerName", "Hero");
int score = Yes2SDK.Data.GetInt("highScore", defaultValue: 0);
bool exists = Yes2SDK.Data.HasKey("highScore");
Yes2SDK.Data.DeleteKey("highScore");
The Set* calls are fire-and-forget, and some platforms batch cloud writes (Yandex debounces them). When you need to know progress is actually stored, such as at a checkpoint, after a purchase, or before the game may close, use the confirmed variants:
// Write one value and wait for the platform to confirm it
Yes2SDK.Data.SetStringAsync("save", json,
onSuccess: saved => { if (!saved) ShowSaveWarning(); },
onError: err => ShowSaveWarning());
// Push every pending write to the backing store
Yes2SDK.Data.FlushAsync(onSuccess: saved => Debug.Log($"Flushed: {saved}"));
// Task overloads take a CancellationToken
bool ok = await Yes2SDK.Data.FlushAsync(CancellationToken.None);
In the Editor both complete immediately with true (PlayerPrefs is saved on the spot). Overlapping calls are fine: each one reports its own result.
Analytics (recommended)
Yes2SDK.Analytics.LogEvent("custom-event", new Dictionary<string, object> {
{ "key", "value" }
});
Yes2SDK.Analytics.LogLevelStart("level-1");
Yes2SDK.Analytics.LogLevelEnd("level-1", score: 1500, success: true);
// Time-based games (racing, time-attack) can include duration:
Yes2SDK.Analytics.LogLevelEnd("level-1", score: 1500, success: true, durationSeconds: 87.3f);
Yes2SDK.Analytics.LogScore(1500);
Session (recommended)
string locale = Yes2SDK.Session.GetLocale(); // e.g. "en", "fr"
string device = Yes2SDK.Session.GetDevice(); // "desktop" or "mobile"
// Data from the link that opened the game, such as referral share data or
// registration prompt data. "{}" or an empty dictionary when there is none.
string entryJson = Yes2SDK.Session.GetEntryPointData();
Dictionary<string, object> entry = Yes2SDK.Session.GetEntryPointDataDictionary();
if (entry.TryGetValue("roomId", out object roomId)) JoinRoom(roomId as string);
In the dictionary, nested objects come back as JObject, arrays as JArray, numbers as long or double, and date-like strings stay strings.
Treat session info as a hint, not a guarantee. Don't branch your core game logic on it.
Optional APIs
These modules add extra player-facing features. They are not guaranteed to be available at runtime: guard with IsSupported() (available on Auth, Friends, Banners, Score, Player, IAP, Referrals and Notifications), and always handle FeatureNotSupported errors gracefully. Don't make your core gameplay depend on them.
Auth
if (Yes2SDK.Auth.IsSupported())
{
Yes2SDK.Auth.GetCurrentUserAsync(
onSuccess: user => Debug.Log($"User: {user.Name}, authenticated: {user.IsAuthenticated}"),
onError: err => Debug.LogError(err)
);
Yes2SDK.Auth.SignInAsync(
onSuccess: user => Debug.Log($"Signed in as {user.Name}"),
onError: err => Debug.LogError(err)
);
}
// Synchronous: false for guests, before init, on platforms without accounts, and on any error.
bool registered = Yes2SDK.Auth.IsAuthenticated();
Registration prompt
ShowRegistrationPrompt shows the platform's sign-up prompt to a guest and returns a handle you wire to your own buttons. When the prompt cannot be shown (player already registered, invalid Message, SDK not initialized, platform without a prompt) it returns null and calls onError right away.
void OfferRegistration()
{
// Save first: the platform may reload the game after registration.
Yes2SDK.Data.SetString("save", SerializeProgress());
Yes2SDK.Data.FlushAsync(onSuccess: _ => ShowSignUpPanel());
}
void ShowSignUpPanel()
{
if (Yes2SDK.Auth.IsAuthenticated()) return;
RegistrationPrompt prompt = Yes2SDK.Auth.ShowRegistrationPrompt(
new RegistrationPromptOptions
{
Theme = RegistrationPromptTheme.Dark,
Data = new Dictionary<string, object> { { "from", "save-slot" } }
},
onClose: () => HideSignUpPanel(),
onError: err => HideSignUpPanel());
if (prompt == null) return; // onError already ran
signUpButton.onClick.RemoveAllListeners();
notNowButton.onClick.RemoveAllListeners();
signUpButton.onClick.AddListener(prompt.Login);
notNowButton.onClick.AddListener(() => { prompt.Close(); HideSignUpPanel(); });
}
Login()hands off to the platform's registration flow, which may reload the game, so save before you prompt. Registration can finish outside the game: checkIsAuthenticated()the next time the game opens.Datacomes back throughSession.GetEntryPointData()after registration.onClosefires at most once per prompt, when the platform reports it closed. Hide your own UI yourself afterClose()rather than waiting for it.Login()andClose()on a closed prompt (IsOpen == false) log a warning and do nothing.Messageis optional. When set it must be non-blank, at most 140 characters, and contain{{registrationCode}}exactly once.- When you show your own prompt on Jest, turn off Automatic login reminders in your game's Overview settings on the Yes2Games Dashboard (available once Jest is enabled for your studio), so the player is not asked twice.
Friends
if (Yes2SDK.Friends.IsSupported())
{
Yes2SDK.Friends.ListFriendsAsync(
page: 0, size: 10,
onSuccess: page => {
foreach (var friend in page.Friends)
Debug.Log($"{friend.Username} ({friend.Id})");
},
onError: err => Debug.LogError(err)
);
}
Referrals
Share a referral link and list the players who joined through it.
if (Yes2SDK.Referrals.IsSupported())
{
Yes2SDK.Referrals.ShareAsync(
new ReferralShareOptions("party_mode_v1")
{
Title = "Play with me",
Text = "Join my party",
Data = new Dictionary<string, object> { { "inviter", playerId } },
NotificationTemplates = new List<ReferralNotificationTemplate>
{
new ReferralNotificationTemplate
{
MinConversionCount = 1,
Variants = { new ReferralNotificationVariant { Body = "A friend joined your party!", CtaText = "Play" } }
}
}
},
onSuccess: result => { if (!result.Canceled) ShowThanks(); },
onError: err => Debug.LogWarning(err));
Yes2SDK.Referrals.ListAsync(
onSuccess: list =>
{
// Send list.SignedRequest to your server and grant rewards from what it verifies.
if (list.Referrals.TryGetValue("party_mode_v1", out var joined))
Debug.Log($"{joined.Count} players joined");
},
onError: err => Debug.LogWarning(err));
}
Referenceis required: a stable campaign key that groups the conversions. Null options or an empty reference fail withInvalidParamsright away.Datareaches the invited player throughSession.GetEntryPointData().ImageDataUrltakes a PNG, JPEG or WebP base64 data URL of at most 2 MiB (seeYes2SDKImage.ToPngDataUrlunder Notifications).OnboardingSlug(optional) routes invited players through an onboarding game first. An empty string fails withInvalidParams. Jest only; ignored elsewhere.NotificationTemplates(optional) are the notifications the referrer gets as invited players join. EachReferralNotificationTemplatehas aMinConversionCount(0 or more) and at least oneReferralNotificationVariant(BodyandCtaTextrequired,TitleandImageReferenceoptional). The platform uses the template with the highest count the referrer has reached and picks one of its variants. A malformed entry fails withInvalidParamsright away. Jest only; ignored elsewhere.- A closed share dialog is a success with
Canceled == true, not an error. ListAsyncgroupsReferralConversions (PlayerId,JoinedAt) by reference. A reference nobody joined through is absent. VerifySignedRequeston your server before granting a reward.
Share an image
Open the platform share sheet with an image, for example a score card or a coupon.
Yes2SDK.Context.ShareImageAsync(
new ContextShareOptions
{
ImageDataUrl = Yes2SDKImage.ToPngDataUrl(scoreCardTexture), // a readable Texture2D
Text = "Beat my score",
Data = new Dictionary<string, object> { { "coupon", "SPRING25" } }
},
onSuccess: () => Debug.Log("Share sheet closed"),
onError: err => Debug.LogWarning(err));
// Image and text only:
Yes2SDK.Context.ShareAsync("Beat my score", Yes2SDKImage.ToPngDataUrl(scoreCardTexture));
- Pass an explicit image, built from a texture with
Yes2SDKImage.ToPngDataUrl(texture). Raw base64 PNG data is also accepted and gets the PNG data URL prefix. With no image some platforms capture the game screen instead, and a WebGL canvas capture can come out blank, so verify it on the device before relying on it. - Null options, or
Datathat cannot be serialized, fail withInvalidParamsright away. Datareaches the player who opens the share throughSession.GetEntryPointData().Textmay be ignored by some platforms.- Success means the share sheet completed or was dismissed: there is no cancel signal.
- Do not gate this on
Context.IsSupported(). It reports context switching and can befalseon a platform where sharing works. Call the share method and handleonError(FeatureNotSupportedwhere sharing is unavailable).
Banners
There are two banner surfaces and they are not interchangeable. Pick by the shape of banner the platform actually offers:
| Surface | Shape | Use when |
|---|---|---|
Ads.ShowBanner(BannerPosition) | One banner, placed top or bottom by the platform. No id, no size. | The platform offers a single sticky banner and accepts no container or size. |
Banners.ShowBanner(id, BannerSize) | Many banners, each in a named container at a size you choose. | The platform renders display ads into containers you lay out yourself. |
They are separate end to end: separate callbacks, separate platform mappings, and no shared state. That last part is why mixing them breaks rather than merely duplicating: hiding on one surface can clear banners the other surface placed, and a refresh can miss one, because each surface tracks only what it placed itself. Pick one surface per game.
Support for either surface varies by platform, and passing a container or size to a platform that has neither means those arguments are ignored. Check the per-method support tables before you commit to one: Ads, Banners.
Only the container surface has a support probe. Banners.IsSupported() answers
before you call; the position-based surface has no equivalent, so an unsupported
platform reports itself through onError after the fact.
// Container-based: many banners, explicit sizes.
if (Yes2SDK.Banners.IsSupported())
{
Yes2SDK.Banners.ShowBanner("sidebar-left", BannerSize.Medium_300x250);
Yes2SDK.Banners.HideBanner("sidebar-left");
Yes2SDK.Banners.HideAllBanners();
}
// Position-based: one sticky banner, platform picks the size.
Yes2SDK.Ads.ShowBanner(BannerPosition.Bottom,
onShown: () => Debug.Log("Banner shown"),
onError: err => Debug.LogError(err));
Yes2SDK.Ads.HideBanner();
Game Extras
HappyTime() signals to the platform that the player just hit a positive moment — level cleared, achievement unlocked, boss defeated. Some platforms use this signal to time monetization prompts and rate requests so they don't interrupt frustrating moments. Call it sparingly, only on genuine highs.
Yes2SDK.Game.HappyTime();
Yes2SDK.Game.InviteLinkAsync(
new Dictionary<string, string> { { "roomId", "abc123" } },
onSuccess: link => Debug.Log($"Invite: {link}")
);
Yes2SDK.Game.ShowInviteButton(new Dictionary<string, string> { { "roomId", "abc123" } });
Yes2SDK.Game.HideInviteButton();
GameSettings settings = Yes2SDK.Game.GetSettings();
Yes2SDK.Game.OnSettingsChanged += s => ApplySettings(s);
Yes2SDK.Game.CopyToClipboard("https://...");
Score
if (Yes2SDK.Score.IsSupported())
{
Yes2SDK.Score.AddScore(150f);
Yes2SDK.Score.SubmitScore("encrypted-score-string");
}
Player Data
if (Yes2SDK.Player.IsDataSupported())
{
Yes2SDK.Player.SetDataAsync("{\"level\":5}", onSuccess: () => {});
Yes2SDK.Player.GetDataAsync(new[] { "level" }, onSuccess: json => {});
Yes2SDK.Player.FlushDataAsync();
}
if (Yes2SDK.Player.IsConnectedPlayersSupported())
{
Yes2SDK.Player.GetConnectedPlayersAsync(onSuccess: json => {});
}
Bot avatars
GetBotAvatarAsync(username, size) returns a platform-generated avatar URL for a computer-controlled player. The username seeds the picture, so the same bot always looks the same. size is "small", "medium" (the default) or "large". An empty username or another size fails with InvalidParams right away. Only some platforms generate bot avatars (Jest today); elsewhere onError gets FeatureNotSupported, so check IsBotAvatarSupported() and fall back to your own art. In the Editor the "Mock referrals, notifications and signed player" toggle returns a placeholder URL that does not load.
if (Yes2SDK.Player.IsBotAvatarSupported())
{
Yes2SDK.Player.GetBotAvatarAsync("RoboRita", "small",
onSuccess: url => LoadAvatar(url),
onError: err => UseDefaultBotArt());
}
else
{
UseDefaultBotArt();
}
Server verification
On platforms that support it, GetSignedPlayerInfoAsync returns a signed proof of who the player is. onSuccess receives a JSON object with playerId and signature; send both to your server and verify the signature there before trusting the id. On platforms without signed player info the call fails with FeatureNotSupported through onError. In the Editor it returns a mock (mock-player and mock-signature) while the "Mock referrals, notifications and signed player" toggle is on.
Yes2SDK.Player.GetSignedPlayerInfoAsync(
"my-payload",
onSuccess: json => SendToServer(json), // {"playerId":"...","signature":"..."}
onError: err => Debug.LogWarning(err));
In-App Purchases
Yes2SDK.IAP.IsSupported() tells you at runtime whether the current platform can take payments. Check it before showing a shop, a "buy" button, or any mechanic that depends on paid items. When it returns false, hide that UI instead of letting the player hit an error.
IAP is available on some platforms only. IsSupported() returns false where it is not, and the calls fail through onError. On Yandex, payments must also be enabled for your game in the Yandex Games console, otherwise the calls fail with a platform error.
if (Yes2SDK.IAP.IsSupported())
{
// On launch: restore what the player already owns, and finish any
// consumable purchase that was paid for but not yet granted.
Yes2SDK.IAP.GetPurchasesAsync(
onSuccess: purchasesJson => RestorePurchases(Purchase.ListFromJson(purchasesJson)),
onError: err => Debug.LogWarning(err));
// Build the shop from the platform catalog (prices are localized).
Yes2SDK.IAP.GetCatalogAsync(
onSuccess: catalogJson => BuildShop(catalogJson), // JSON array
onError: err => HideShop());
}
else
{
HideShop();
}
// When the player taps "buy":
Yes2SDK.IAP.PurchaseAsync("gems_100",
onSuccess: purchaseJson =>
{
var purchase = Purchase.FromJson(purchaseJson);
GrantItem(purchase.ProductId);
// Sandbox purchases move no real money: grant the item, but keep it
// out of your revenue reporting.
if (purchase.IsSandbox) MarkAsTestPurchase(purchase);
// Consumables must be consumed so they can be bought again.
Yes2SDK.IAP.ConsumePurchaseAsync(purchase.PurchaseToken);
},
onError: err =>
{
// Yandex reports a closed payment dialog the same way as a failed
// payment, so keep this message neutral ("Purchase not completed").
// Platforms that report a closed checkout separately send it as
// err.ErrorCode == ErrorCode.UserCancelled.
if (err.ErrorCode == ErrorCode.UserCancelled) return;
ShowPurchaseNotCompleted();
});
- Results arrive as JSON strings. Parse a purchase with
Purchase.FromJson(json)and aGetPurchasesAsyncarray withPurchase.ListFromJson(json); both return null or an empty list and log a warning on bad input. APurchasecarriesProductId,PurchaseToken,PaymentId,PurchaseTime(ISO 8601),DeveloperPayload,SignedRequestandIsSandbox.GetCatalogAsyncreturns a top-level JSON array, whichJsonUtilitycannot parse on its own: wrap it (JsonUtility.FromJson<Wrapper>("{\"items\":" + json + "}")) or use Newtonsoft. Each catalog product carriesproductId,title,description,price(formatted) andpriceCurrencyCode. IsSandboxis true when no real money changed hands (a sandbox tester or the platform simulator, and always in the Editor mock). Grant the item, but keep it out of revenue reporting.SignedRequestis the platform's signed proof of the purchase, or null when the platform provides none. Verify it on your server before granting value for anything that matters.- Recover incomplete purchases on startup with
GetPurchasesAsync(see the launch example above), then grant and consume them. - Grant the item before consuming it, and save progress (see
Data.FlushAsync) so a closed tab can't lose a paid item. Any purchase that was paid but not consumed comes back fromGetPurchasesAsyncon the next launch. - In the Editor, IAP is mocked in Play Mode (see Editor Testing), so you can test your shop and your
IsSupported()gating without a platform build.
Subscriptions
Subscriptions are not available on every platform that supports IAP: gate them with IsSubscriptionSupported(), not IsSupported(), and read the typed results.
if (Yes2SDK.IAP.IsSubscriptionSupported())
{
// On every launch: the list is the source of truth for entitlements.
Yes2SDK.IAP.GetSubscriptionsAsync(
onSuccess: subscriptions =>
{
foreach (Subscription sub in subscriptions)
{
if (sub.IsActive) GrantVip(sub); // never re-offer it
else ShowOffer(sub, showTrial: sub.TrialEligible);
}
},
onError: err => HideSubscriptions());
}
// When the player taps "subscribe":
Yes2SDK.IAP.SubscribeAsync("vip_monthly",
onSuccess: result => { if (result.IsSubscribed) GrantVip(result.Subscription); },
onError: err => ShowPurchaseNotCompleted());
- Grant access when
IsActiveis true. Show trial copy only whenTrialEligibleis true.IntroOfferandRetentionOfferare null when there is none.IsSandboxandSignedRequestwork as onPurchase. - A closed checkout is a success with
result.Status == SubscribeStatus.Cancelled, not an error. - Never offer a subscription the player already holds:
SubscribeAsyncfails witherr.Code == "IAP_ALREADY_PURCHASED". - Guests may get an empty list, and
SubscribeAsyncfails witherr.Code == "PLAYER_NOT_AUTHENTICATED". Offer a registration prompt instead. CancelSubscriptionAsync(productId)reportstruewhen the player confirmed andfalsewhen they dismissed the dialog. The player keeps access until the end of the billing period, so do not revoke it at once.ClaimRetentionOfferAsync(productId)appliesRetentionOfferto a subscription the player holds and returns the refreshedSubscription. Repeating a claim is safe. A player who does not hold it getserr.Code == "INVALID_OPERATION".
Notifications
if (Yes2SDK.Notifications.IsSupported())
{
Yes2SDK.Notifications.ScheduleAsync(
new NotificationOptions
{
Id = "daily-reward", // scheduling the same id again replaces it
Title = "Your reward is ready",
Body = "Come back and claim today's chest.",
ScheduledInDays = 1, // or DelaySeconds: exactly one of the two
CtaText = "Claim",
Priority = NotificationPriority.High,
ImageDataUrl = Yes2SDKImage.ToPngDataUrl(chestTexture),
Data = new Dictionary<string, object> { { "reward", "chest" } }
},
onSuccess: scheduled => Debug.Log($"Scheduled {scheduled.Id} at {scheduled.ScheduledAt}"),
onError: err => Debug.LogWarning(err));
Yes2SDK.Notifications.CancelAsync("daily-reward");
}
A 7-day return loop is one call per day:
for (int d = 1; d <= 7; d++)
Yes2SDK.Notifications.ScheduleAsync(
new NotificationOptions { Id = $"day{d}", Title = "We miss you", Body = "Your rewards are waiting.", ScheduledInDays = d });
| Option | Rule |
|---|---|
Title | Required, at most 200 characters. May be empty: the notification is then sent without a title. |
Body | 1 to 2000 characters. |
DelaySeconds | Positive, at most 7 days. Set this or ScheduledInDays, exactly one. |
ScheduledInDays | Whole days from now, 0 to 7. The platform picks the best time inside that day. |
Id | Optional. Generated when null; reusing an id replaces that notification. An empty string is rejected. |
CtaText | Optional button label, 1 to 50 characters. |
Priority | Low, Medium (default), High or Critical. |
ImageAssetId / ImageDataUrl | At most one. ImageDataUrl is a PNG, JPEG or WebP base64 data URL with a lowercase prefix, at most 2 MiB. |
IconUrl | Optional icon URL, on platforms that use one. |
Data | Optional data handed back to the game when the player opens the notification. |
Yes2SDKImage.ToPngDataUrl(texture)builds anImageDataUrlfrom a readable texture. It returns null and logs a warning when the texture is not readable (enable Read/Write) or encodes to more than 2 MiB; a null image is simply not sent.- Options that break these rules fail with
InvalidParams. On platforms that only notify registered players, a guest getserr.Code == "PLAYER_NOT_AUTHENTICATED". - Keep
ScheduledNotification.Idto cancel later.CancelAllAsync()cancels every notification the game scheduled (some platforms only reach the ones scheduled this session). - The older
ScheduleAsync(title, body, delaySec, dataJson, ...)overload still works and passes the new id toonSuccess. PreferNotificationOptions.
Jest
Yes2SDK runs on Jest (jest.com) too. Yes2SDK.GetPlatform() returns Platform.Jest there.
- Supported: player, signed player and bot avatars, Auth with the registration prompt (guests only), Data (1 MB), lifecycle and
OnExitRequested, IAP and subscriptions, Notifications (ScheduledInDays0 to 7, with images), the entry payload (Session.GetEntryPointData()), Referrals, sharing an image, and analytics (logged only). - Not supported: banners, leaderboards, achievements, tournaments, stats, review, config, friends and score. Check
IsSupported()and handleFeatureNotSupported. - Ads: there are none.
Ads.IsInterstitialSupported()andAds.IsRewardedSupported()returnfalse. If you callAds.ShowInterstitialorAds.ShowRewardedanyway,onErrorfires withNoFill, so resume inonErrorand never gate progress on an ad.
Launch checklist:
- Progress is saved as a guest before any login prompt
- Notifications are scheduled for registered players on days 1 to 7, with images
- Incomplete purchases are recovered and completed at startup with
GetPurchasesAsync - A held subscription (check
GetSubscriptionsAsync) is never offered again - Guests get a registration prompt (with a custom prompt, turn off Automatic login reminders in the game's Overview settings on the Yes2Games Dashboard)
- Progress is saved synchronously in
OnExitRequested - Set Jest's loading screen to Auto mode in its developer console (recommended for now), and still call
SetLoadingProgressas assets load andStartGameAsyncwhen playable. In Manual mode Jest closes the game after 15 seconds without a progress update, and Yes2SDK only keeps it alive once it has initialized, so a long engine download can hit that timeout - Asset paths are relative
- Entry data comes from
Session.GetEntryPointData(), not URL parameters
Do not install Jest's own Unity package or Jest's SDK script next to Yes2SDK: Yes2SDK loads the platform SDK itself, and two copies conflict. The Jest platform guide is being published in the Yes2Games docs.
Control plane (opt-in)
The control plane client is off by default. Without opt-in its assembly, Yes2SDK.ControlPlane, is not compiled, adds nothing to your build, and the package needs no networking module.
To opt in:
- Add
YES2SDK_CONTROL_PLANEto Scripting Define Symbols (Project Settings > Player > Script Compilation) for every target you build, or to your Unity 6 Build Profile's scripting defines. A-define:YES2SDK_CONTROL_PLANEline inAssets/csc.rspsets it for every target at once. - Keep the built-in UnityWebRequest module enabled (Window > Package Manager > Built-in). It is on by default; turn it back on if your project disabled it.
With the define you get the Yes2SDK.ControlPlane assembly: the strict JSON reader, the SHA-256 digest check, and the UnityWebRequest transport that fetches the bootstrap and snapshot documents with bounded retries. It has no public API yet. The Yes2ControlPlane entry point is coming, and it will exist only under the same define. You need no link.xml entry for it: the assembly uses no reflection, so managed stripping keeps everything your calls reach.
Integration Checklist
Your build is ready for review when:
-
InitializeAsyncis called at startup -
SetLoadingProgressis called as assets load -
StartGameAsyncis called when the game is playable -
OnPause/OnResumeare handled (mute audio, pause gameplay) - Interstitial ads run at natural break points
- Rewarded ads grant reward only in
adViewed -
Game.GameplayStop()is called before every ad;Game.GameplayStart()after - Gameplay resumes in
afterAdANDonError -
Datais used for persistent player data
The QA Inspector in the Yes2Games Dashboard validates all of this automatically.
Building
- Open Yes2SDK > Build Window
- Click Build WebGL (or Build and Run to launch in browser).
The output folder is what you upload to the Yes2Games Dashboard — the dashboard handles SDK injection, platform bundling, and walks you through the QA Inspector and review request.
Build Configuration
| Setting | Recommended | Notes |
|---|---|---|
| Template | Yes2SDK-SuperSDK | Required. The build-time guard fails the build with any other template. |
| Compression | Disabled | Required for dashboard upload — the CDN doesn't currently send Content-Encoding headers. |
| Code Stripping | Medium | Balances build size and AOT safety. |
| Exception Support | Explicitly Thrown Exceptions Only | See note below. |
| Memory Size | 256 MB+ | Most games need at least 256–512 MB. Smaller heaps trigger a generic "unspecified error" at boot. |
⚠️ Why not
Exception Support: None?
Noneproduces the smallest build, but Unity WebGL'sNonemode strips exception infrastructure entirely — even an exception caught by atry/catchaborts the wasm. This breaks any code path where a dependency usestry/catchas control flow (Newtonsoft.Json — which the SDK itself imports — third-party Unity asset packages, save systems, etc.).Most games should ship with Explicitly Thrown Exceptions Only: about 10% larger build but compatible with the .NET ecosystem. Use
Noneonly after auditing your full dependency graph.
Where to set these
- Yes2SDK Build Window (recommended) — Yes2SDK > Build Window > WebGL Settings (collapsible panel). Edits write to Player Settings live, no Apply step. Click Reset to recommended to apply all the values from the table above at once.
- Unity 2021–2022: Edit > Project Settings > Player > WebGL (project-wide).
- Unity 6+: File > Build Profiles — select your WebGL profile, then click Player Settings at the bottom of the profile panel. Settings apply only to that profile.
Unity 6 moved WebGL settings from project-wide Player Settings into per-profile Build Profiles. The Yes2SDK Build Window's Settings panel reads and writes the project-wide Player Settings. If you have an active Build Profile with overrides for these settings, the profile takes precedence at build time — edit those overrides via File > Build Profiles. The panel will surface a notice when an active profile is detected so you don't edit values that are then ignored.
Build Mode for diagnostics
The Build Window has a Build Mode dropdown for one-off overrides without changing your Player Settings:
- Production — use Player Settings as-is. Default for shipping builds.
- Production Safe — temporarily forces Exception Support to
Explicitly Thrownfor one build. Useful when your Player Settings isNonebut you need a build that catches third-party throws. - Diagnostic — temporarily forces
Full With Stacktrace. Use to capture real C# class/method names in browser console errors when chasing a crash.
The override saves and restores Player Settings around each build, so it never leaves your project in an unexpected state.
Editor Testing
In the Unity Editor, SDK calls run against mock implementations:
InitializeAsync/StartGameAsyncsucceed immediately- Ads show a fullscreen mock ad in Play Mode: a countdown (3s interstitial, 5s rewarded), then Close Ad (fires
afterAd), or Claim Reward (firesadViewed) / Skip (firesadDismissed) for rewarded ads. This lets you verify pause-resume wiring and both reward outcomes by clicking. The ad fills the game view and scales with its resolution, landscape or portrait. While the popup is up, input to the game behind it is blocked (uGUI clicks, legacyInputaxis/button polling), matching how a real ad overlay behaves. Direct new Input System device polling (e.g.Keyboard.current) is not suppressed. - IAP is mocked in Play Mode:
IsSupported()returns true,GetCatalogAsyncreturns a sample catalog, andPurchaseAsyncopens a Buy / Cancel dialog. Any product id is accepted, so you can test with your real ids. Purchases last for the current play session. - Failures can be simulated: the Ad result dropdown (No fill / Ad blocked / Error) makes ad calls fire
onErrorwith platform-shaped error codes, and Fail purchases makesPurchaseAsyncfail, so error handling is testable without a platform build. - Subscriptions follow the IAP mock:
SubscribeAsyncopens a Subscribe / Close dialog andCancelSubscriptionAsynca confirm dialog. The mock applies the platform rules (guest, already held, not held) so those error paths are testable. - Player is registered sets whether the mock player is signed in (off, a guest, by default). It drives
IsAuthenticated(), the subscription list and the guest errors. The registration prompt is mocked without UI:Login()registers the player for the current play session andClose()closes the prompt. - Entry point data (JSON) is what
Session.GetEntryPointData()returns in Play Mode. Only a valid JSON object is saved. - Mock referrals, notifications and signed player turns on those mocks, the image share mock and the bot avatar mock (Play Mode). The image share, signed player and bot avatar mocks always succeed; the bot avatar URL is a placeholder that does not load. Referral share result picks Shared / Cancelled / Error, and Referral conversions sets how many players joined through each shared reference; with 0, references are left out of
ListAsyncresults, as on a real platform. - Simulate exit request (Play Mode only) raises
OnExitRequested, then saves game data the way the platform flush does. DatausesPlayerPrefs- Other optional APIs return
FeatureNotSupported
The mocks can be turned off under Yes2SDK > Build Window > Play Mode Testing. With the ad popup off, ad callbacks fire instantly with no UI (pass "dismiss" as the rewarded description to trigger adDismissed). Batch-mode runs (CI) always use the instant flow.
For richer simulation (specific locales, network conditions, event log capture), use the QA Inspector in the Yes2Games Dashboard.
Automated tests
The package ships EditMode tests covering the ad callback contract: callback order, and the in-flight teardown that keeps one bad ad from blocking every later one. They run on the instant flow, so they need no rendering and work in batch mode.
To see them in a consuming project, add the package to testables in that project's Packages/manifest.json, and add the test framework there too — the package does not depend on it, because a shipping game has no reason to inherit test infrastructure:
{
"dependencies": {
"com.unity.test-framework": "1.4.6"
},
"testables": [
"com.yes2games.yes2sdk"
]
}
ci~/consumer is the committed minimal project that does exactly this, and it is what the required CI lanes run the tests through.
They then appear under Window > General > Test Runner > EditMode. Headless:
Unity -batchmode -nographics -projectPath <project> -runTests -testPlatform EditMode -testResults results.xml
testables is a project-manifest field, so a package cannot opt itself in; each consuming project adds the line.
Error Handling
All async methods accept an onError callback with an Error struct:
public struct Error
{
public string Code;
public string Message;
public string Context;
public ErrorCode ErrorCode;
}
public enum ErrorCode
{
NotInitialized, InvalidParams, FeatureNotSupported, PlatformError,
NetworkError, RateLimited, UserCancelled, Unknown
}
Use ErrorCode for control flow:
onError: err => {
if (err.ErrorCode == ErrorCode.FeatureNotSupported)
// gracefully hide the feature
else
Debug.LogError(err);
}
Error code reference
err.ErrorCode is the typed code to branch on. It reads both the SDK's own codes and the platform runtime's codes (such as PLATFORM_ERROR or NETWORK_FAILURE), while err.Code keeps the original string for logging.
| Code | When it fires | Recommended handling |
|---|---|---|
NotInitialized | An API was called before InitializeAsync succeeded. | Wait for init to complete first; never call SDK methods from Awake() without checking Yes2SDK.IsInitialized. |
InvalidParams | A required parameter was null/empty or out of range. | Treat as a programmer error — fix the call site. |
FeatureNotSupported | The optional API isn't available on this platform (e.g. Friends on Poki). | Hide the related UI; fall back to a non-platform alternative. Always pre-check with IsSupported() for optional APIs. |
PlatformError | The underlying platform SDK rejected the call. | Log err.Message and err.Context for support; treat the call as failed. |
NetworkError | A platform call failed network-side (timeout, offline, server error). | Retry with backoff. Don't retry indefinitely. |
RateLimited | Too many calls in a short window (e.g. ad spam protection). | Back off and try again later — don't retry immediately. |
UserCancelled | The player closed/dismissed a flow (e.g. login dialog, rewarded ad, purchase checkout). | Not an error in the usual sense - silently respect the player's choice, no toast. |
Unknown | The error didn't match any of the above. | Log everything (err.Code, err.Message, err.Context) and treat as a hard failure. |
Timeout | Raised by the SDK's ad watchdog: an interstitial or rewarded ad never started, or started and never finished. | Treat the ad as failed and resume the game. The next Show* works normally. |
Running alongside other SDKs
Real games often ship with multiple platform SDKs in the same build (Yes2SDK + Poki + Yandex + Playgama, etc.). A few ground rules to keep them from stepping on each other:
- Init order. Initialize Yes2SDK first. Yes2SDK figures out which actual platform is hosting the game and routes through it, so initializing your own platform SDK directly first can race with Yes2SDK's detection.
- One owner for pause / resume. Pick one SDK to drive
Time.timeScaleandAudioListener.pause. If both Yes2SDK and another SDK call resume/pause, you'll get oscillation. Recommended: subscribe toYes2SDK.OnPause/OnResumeand ignore the other SDK's equivalent. - One owner for ads. Don't call ads via two SDKs in the same session — the platform almost always rejects the second call. Pick the SDK that targets the platform you're actually hosted on.
- Namespace collisions. If you have your own
Platformtype, qualify the Yes2SDK enum (Yes2SDK.Platform) at the call site or use ausingalias (using Y2 = Yes2SDK;). C# resolves namespace-vs-type ambiguity by full qualification. - Init timeout. If you depend on Yes2SDK init completing before your other SDK's flow, wrap
InitializeAsyncin aCancellationTokenwith a timeout (seeawait-friendly overloads) so your game doesn't hang on a wedged JS bridge.
Contributing
After cloning, point Git at the committed hooks once:
git config core.hooksPath .githooks
.githooks/pre-push then refuses a direct push to the default branch, so changes go through a pull request and face YES2 Unity Required CI.
It is a soft guard, not enforcement. It fires only in a clone that has run the command above, and git push --no-verify bypasses it outright. Nothing protects main server-side: this organization's plan and the deliberately uniform cross-SDK contract mean there is no branch protection, no ruleset and no CODEOWNERS gate on any Yes2 SDK (yes2games/yes2dashboard#141 sections 3 and 14). Read the checklist as "a mistake is caught", never as "main is protected".
License
MIT — see LICENSE.