// With Unity IAP, present when com.unity.purchasing is installed:
if (BestBridgeStoreUtilityIAP.IsStoreInRuRegion(product)) { /* route through BestBridge */ }
// Or pass the ISO currency code directly:
if (BestBridgeStoreUtility.IsStoreInRuRegion("RUB")) { /* route through BestBridge */ } Unity | 2021.3 or newer |
Dependencies | none (UnityWebRequest + JsonUtility only) |
Credentials | a project API key, issued at onboarding |
Environment | development and production are both built into the SDK; isDevBuild picks one, environment forces one |
using BestBridge.Payments;
var config = new BestBridgeConfig(
apiKey: "your-project-api-key",
isDevBuild: Debug.isDebugBuild, // selects the built-in development / production environment
identities: IdentitiesListCreator.Create(
new DeviceIdIdentityProvider()
// , new GooglePlayIdentityProvider(googlePlayId) // a custom IIdentityProvider
)); var products = new[]
{
new BestBridgeProduct("coins_small", "sku_coins_500", BestBridgeProductType.Consumable),
new BestBridgeProduct("remove_ads", "sku_remove_ads", BestBridgeProductType.NonConsumable),
new BestBridgeProduct("premium", "sku_premium_1m", BestBridgeProductType.Subscription),
}; public class GamePurchaseListener : IBestBridgePurchaseListener
{
public async Task<bool> OnPurchase(BestBridgePurchaseInfo purchase)
{
// _wallet, _ui and _profile belong to the game, not to the SDK:
// _wallet.Grant — adds the product to the player's inventory or balance
// _ui.PlayPurchaseCelebration — plays the "purchase successful" animation
// _profile.Save — writes the player profile to disk / cloud, true when it persisted
_wallet.Grant(purchase.Product, purchase.Quantity); // × Quantity — multi-buy!
if (!purchase.IsRestored)
_ui.PlayPurchaseCelebration(purchase.Product); // skip celebration on restores
bool saved = await _profile.Save();
return saved; // false → SDK retries later
}
} Situation | Return |
Granted and persisted | true |
Could not persist (network or error) | false — roll back the local grant |
A subsystem is not ready yet | false |
Exception thrown | treated as false |
Interface | What it is |
IBestBridgePopupController | Factory: ShowWaitPopup(bool canCancel), ShowConfirmationPopup(), ShowInfoPopup() |
IBestBridgeWaitPopup | “Please wait” while the order is created and polled. Its CancellationToken cancels the purchase; Close() is called by the SDK. |
IBestBridgeConfirmationPopup | RequestConfirmation (request) → confirmed + chosen quantity. Shown before any order exists. |
IBestBridgeInfoPopup | ShowSuccess, ShowError, ShowRestoreResult. Every failure the SDK reports arrives at ShowError with a ready message. |
IBestBridgePaymentsService service = new BestBridgePaymentsService(
products,
new GamePurchaseListener(),
popupController, // the game's IBestBridgePopupController implementation; required, non-null
config);
BestBridgeInitializationStatus status = await service.Initialize();
if (status.IsSuccess) Debug.Log("BestBridge ready");
else if (status.IsConnectionError) Debug.LogWarning("No connection — the SDK will retry itself");
else if (status.IsCancelled) Debug.Log("Initialization cancelled");
else Debug.LogError(status.Exception); service.OnInitialized += status => { if (status.IsSuccess) Debug.Log("ready"); };
service.Initialize().Forget(); var result = await service.Purchase("coins_small"); // one unit
var result = await service.Purchase("coins_small", quantity: 3); // preselect x3
// RedrawShop() is a method of the game, not of the SDK: it repaints the shop screen
// (prices, "owned" marks, currency counter) after the balance changed.
if (result.IsDelivered) RedrawShop(); // the SDK has already told the player the outcome // MarkOwnedInShop() is a method of the game, not of the SDK: it switches the shop item
// to the "owned" look — no price, no buy button.
if (service.IsNonConsumablePurchased("remove_ads")) MarkOwnedInShop(); // button, label, dateRow and warningRow are the game's own UI objects on the subscription
// screen; LabelFor() is the game's own method that turns an action kind into localized text.
var action = service.GetSubscriptionAction("premium");
button.interactable = action.IsEnabled;
label.text = LabelFor(action.Kind); // e.g. Subscribe → "Подписаться"
dateRow.SetActive(action.ShowsExpiration); // render action.Data.ExpirationDate
warningRow.SetActive(action.HasPaymentProblem);
button.onClick.AddListener(async () => await service.InvokeSubscriptionActionAsync("premium")); BestBridgeSubscriptionState | Meaning | What to show |
Unknown | Nothing synced yet | Loading or disabled. Never “Subscribe” |
NotSubscribed | No live subscription | “Subscribe” → Purchase(productId) |
Active | Subscribed, auto-renew on | “Unsubscribe” → Unsubscribe(productId); optionally NextBillingDate |
CanceledUntilPeriodEnd | Auto-renew already off | “Active until ExpirationDate”, not an unsubscribe button |
Paused | Paused, access removed | “Resume” → ResumeSubscription(productId) |
Grace | A renewal charge failed, access kept for now | Nothing required: the SDK shows its own popup |
// Every Show*() below is a method of the game, not of the SDK: each one puts the subscription
// screen into one visual state (subscribe button, unsubscribe button with the next charge date,
// "active until <date>", resume button, payment-problem notice, loading spinner).
switch (service.GetSubscriptionState("premium", out var sub))
{
case BestBridgeSubscriptionState.NotSubscribed: ShowSubscribeButton(); break;
case BestBridgeSubscriptionState.Active: ShowUnsubscribeButton(sub.NextBillingDate); break;
case BestBridgeSubscriptionState.CanceledUntilPeriodEnd: ShowActiveUntil(sub.ExpirationDate); break;
case BestBridgeSubscriptionState.Paused: ShowResumeButton(); break;
case BestBridgeSubscriptionState.Grace: ShowPaymentProblem(sub.ExpirationDate); break;
default: ShowSpinner(); break; // keep this
} Moment | What to do |
The subscription screen or shop opens (OnEnable) | Draw from the cache at once, subscribe to OnSubscriptionsChanged, then await service.RefreshSubscriptionStatusAsync() and redraw. |
Return to the foreground while such a screen is open | await service.RefreshSubscriptionStatusAsync(). It picks up an expiry, a renewal or a cancellation made on another device. |
The screen closes (OnDisable / OnDestroy) | Unsubscribe from OnSubscriptionsChanged. |
After Unsubscribe, ChangeSubscriptionPlan or InvokeSubscriptionActionAsync | Nothing. These re-sync themselves and raise OnSubscriptionsChanged. |
// Redraw() is the game's own method: it re-reads GetSubscriptionAction / GetSubscriptionState
// and rebuilds the whole subscription screen from what it finds.
private void OnEnable()
{
_service.OnSubscriptionsChanged += Redraw;
Redraw(); // cached state, drawn without waiting for the network
RefreshAsync().Forget(); // BestBridgeTaskExtensions.Forget()
}
private void OnDisable() => _service.OnSubscriptionsChanged -= Redraw;
private async Task RefreshAsync()
{
await _service.RefreshSubscriptionStatusAsync();
Redraw();
} IReadOnlyList<BestBridgeProductInfo> catalog = await service.GetProductsAsync(); // no auth needed BestBridgeProductInfo info = service.GetProductInfo("coins_small");
string price = service.GetLocalizedItemPrice("coins_small"); // "" if not cached yet await service.RestorePurchases(showPopupOnResult: true); await service.TryProcessUnfinishedPurchaseAsync(); // true if at least one product was delivered // settingsUserIdLabel is the game's own UI label on the settings screen, not part of the SDK.
settingsUserIdLabel.text = $"User ID: {service.UserId}"; // add a copy button next to it
public sealed class MyTexts : BestBridgeMessagesDefaults
{
public override string PurchaseSuccess => "Спасибо за покупку!";
public override string ConnectionProblem => "Нет соединения";
}
var service = new BestBridgePaymentsService(products, listener, popupController, config, new MyTexts());