Skip to main content

Store Integration - iOS

An example of implementing brainCloud Server-to-Server Verification for Apple StoreKit 2 with Unity In-App-Purchasing service

Jason Liang avatar
Written by Jason Liang
Updated today

Overview

brainCloud VerifyPurchase is used to ensure that in-app purchases are trusted, authoritative, and managed by the backend rather than the client. Based on the verified result, the backend can safely grant consumables, persist non-consumable entitlements, and manage subscription state and expirations. It is not just about rewarding items, but about making the server the single source of truth for what a user actually owns.


This guide covers the integration of Apple In-App Purchases using Unity IAP (StoreKit 2) alongside brainCloud’s server-side verification.


What's New in StoreKit 2?

  • JWS-based receipts - Signed JSON tokens replace legacy base64 receipts

  • Transaction IDs - Unique identifiers for each purchase (recommended for brainCloud verification)

  • Improved subscription management - Better renewal and cancellation tracking

  • Server-to-Server notifications - Real-time purchase updates from Apple


Apple App Store Connect Setup

Step 1: Create Your App in App Store Connect

  1. Navigate to My Apps → Click +New App

  2. Fill in required fields:

    1. Platform: iOS

    2. App Name

    3. Primary Language

    4. Bundle ID (must match Unity's Bundle Identifier)

    5. SKU (unique identifier)

Step 2: Configure In-App Purchases

  1. In your app's page, click In-App Purchases (left sidebar), or Subscriptions (for subscription purchases)

  2. Click + to create a new in-app purchase or subscription

Creating Different Product Types

Consumable (e.g., Coins)

Type: Consumable
Reference Name: 100 Coins Pack
Product ID: coins_100
Price: Select tier

Non-Consumable (e.g., Remove Ads)

Type: Non-Consumable
Reference Name: Remove Ads
Product ID: remove_ads
Price: Select tier

Auto-Renewable Subscription (e.g., VIP Membership)

Type: Auto-Renewable Subscription
Subscription Group: Create new (e.g., "VIP Membership")
Reference Name: Monthly VIP
Product ID: vip_monthly
Subscription Duration: 1 Month
Price: Select tier

Non-Renewing Subscription (e.g., Event Pass)

Type: Non-Renewing Subscription
Reference Name: 7-Day Event Pass
Product ID: event_pass_7days
Price: Select tier

Step 3: Configure Sandbox Testers

  1. Go to App Store ConnectUsers and AccessSandbox tab

  2. Click + to add a tester if you don't have any

  3. Create test Apple IDs (use unique, non-existing email addresses)

  4. Save tester credentials for testing

Step 4: Configure purchase integration keys

  1. Navigate to App Store ConnectUsers and AccessIntegrations tab

  2. Generate an In-App Purchase key for consumable and non-consumable products. Record the Issuer ID, Key ID, and the contents of the downloaded encoded key for configuration within the brainCloud portal.

  3. Generate a Shared Secret for subscription purchases if required for your implementation.


Unity Project Setup

Installation

  1. Unity IAP: Package Manager → Unity Registry → In-App Purchasing service (v5.1.2+)

  2. brainCloud SDK: Package Manager → Add from GitHub URL

Configuration

iOS Player Settings:

  • Bundle Identifier must match App Store Connect

  • Target iOS 15.0+ for StoreKit 2 support

Step 1: Install Unity IAP

  1. Open Unity Editor

  2. Go to WindowPackage Manager

  3. Unity Registry → In-App Purchasing

  4. Install version 5.1.2 or newer

Step 2: Install brainCloud SDK

  1. Open Unity Editor

  2. Go to WindowPackage Manager

  3. Click + → Install package from git URL...

  4. pass https://github.com/getbraincloud/braincloud-unity-package.git

Step 3: Configure Unity Project Settings

iOS Player Settings

File → Build Settings → iOS → Player Settings

Player Settings:
├── Company Name: [Your Company]
├── Product Name: [Your App Name]
└── Bundle Identifier: com.yourcompany.yourapp (must match App Store Connect)

Other Settings:
├── Target minimum iOS Version: 15.0 or higher
├── Architecture: ARM64
└── Automatically Sign: Enable (or configure manual signing)

Identification:
└── Signing Team ID: [Your Apple Team ID]

brainCloud Configuration

Step 1: Create brainCloud Application

  1. Create a new app or use an existing one

  2. From App > Design > Core App Info page, note your app credentials and apply them to your Unity project

    1. App ID: Found in portal Core App InfoApplication IDs

    2. App Secret: Found in Core App InfoApplication IDs

  3. Navigate to the Apple tab under the Configure Platforms section and complete the required fields:

    1. Bundle ID must match your app identifier in your Apple Developer account and Unity's Bundle Identifier

    2. Enter the In-App Purchase Shared Secret (required only for auto-renewable subscriptions)

    3. Enter the Apple ID for your application, which can be found in App Store Connect under General → App Information section

    4. Enter the Issuer ID, Key ID, and encoded key from the Configure purchase integration keys step

    5. Leave the Use App Store Server API for legacy receipts (optional) checkbox unchecked if you are not using the old receipt-based IAP system, instead of a transaction-based system

Step 2: Configure Products in brainCloud

Critical: Products Identifiers in brainCloud must exactly match your Apple App Store Connect products.

  1. In brainCloud portal, navigate to: DesignMarketplaceProducts

  2. Click + Add Product for each IAP item

Product Configuration Fields

For each product, configure:
Product ID (Required)

  • Must match Unity product constant (e.g., coins_100)

  • This is your internal identifier

iTunes Product ID (Critical for Apple)

  • Must exactly match the Product ID in App Store Connect

  • This is what brainCloud uses to verify with Apple

  • Case-sensitive!

Product Type:

  • Consumable - Can be purchased multiple times (coins, lives, etc.)

  • Non-Consumable - One-time purchase (remove ads, unlock features)

  • Subscription - Use for both auto-renewable and non-renewing subscriptions

Category:

  • Currency - For virtual currency (coins, gems)

  • Item - For consumable items (power-ups, boosts)

  • Unlock - For unlockable content (levels, characters)

Price: Enter the price in your default currency (for reference/analytics)

Example: Consumable Product (Coins)

Product ID: coins_100
iTunes Product ID: coins_100
Type: Consumable
Category: Currency
Price: 0.99
Description: 100 gold coins

Example: Non-Consumable Product (Remove Ads)

Product ID: remove_ads
iTunes Product ID: remove_ads
Type: Non-Consumable
Category: Unlock
Price: 2.99
Description: Remove all advertisements

Example: Subscription Product (VIP Monthly)

Product ID: vip_monthly
iTunes Product ID: vip_monthly
Type: Subscription
Category: Unlock
Price: 9.99
Description: Monthly VIP membership

Important Notes:

  • You can also configure product data (e.g., how many coins to grant) in the brainCloud product definition

  • This allows you to change rewards server-side without app updates

  • Use the Data field (JSON) to store product-specific configuration

Adding Product Data (Optional but Recommended)

In the Data field, you can add JSON configuration:

{
"coins": 100,
"bonus_coins": 10,
"icon": "coin_pack_100"
}

This data is returned in the VerifyPurchase response, allowing server-controlled content delivery.

  • In the brainCloud portal, go to DesignMarketplaceProducts

  • Click + to add products matching your App Store Connect products:

Product ID: coins_100
Type: Consumable
Category: Currency
Price: [Match App Store pricing]
iTunes Product ID: coins_100 (must match exactly)
  • Repeat for all products:

    1. coins_100 (Consumable)

    2. remove_ads (Non-Consumable)

    3. vip_monthly (Subscription)

    4. event_pass_7days (Subscription)


Code Implementation Guide

Step 1: Script Setup and Initialization

Create a new C# script (e.g., IAPManager.cs) and attach to a GameObject in your scene.

Core Components Declaration

using UnityEngine;
using UnityEngine.Purchasing;
using BrainCloud;
using System.Collections.Generic;
using System.Linq;

public class IAPManager : MonoBehaviour
{
// Unity IAP Controller
private StoreController storeController;

// brainCloud Wrapper
private BrainCloudWrapper bc;

// Receipt tracking for verification
private string lastReceipt;
private string lastTransactionId;

// Product IDs (must match App Store Connect)
public const string COINS = "coins_100";
public const string REMOVE_ADS = "remove_ads";
public const string VIP = "vip_monthly";
public const string EVENT_PASS = "event_pass_7days";
}

Key Points:

  • StoreController is the new Unity IAP v5 controller (replaces the old IStoreController)

  • Product IDs must exactly match App Store Connect Product IDs

  • lastTransactionId captures StoreKit 2 transaction IDs for verification

Step 2: brainCloud Initialization

void InitBrainCloud()
{
// Get or add BrainCloudWrapper component
bc = gameObject.GetComponent<BrainCloudWrapper>();
if (bc == null)
{
bc = gameObject.AddComponent<BrainCloudWrapper>();
}

bc.WrapperName = gameObject.name;

// Initialize with your brainCloud credentials
bc.Init(
"<REDACTED> // Server URL
"YOUR_APP_ID", // Your App ID
"YOUR_APP_SECRET", // Your App Secret
"1.0.0" // App Version
);

bc.Client.EnableLogging(true); // Enable for debugging
bc.SetAlwaysAllowProfileSwitch(true);

Debug.Log("brainCloud initialized");
}

Replace placeholders:

  • YOUR_APP_ID: From the brainCloud portal

  • YOUR_APP_SECRET: From the brainCloud portal

Call in Start():

void Start()
{
InitBrainCloud();
InitializePurchasing();
}

Step 3: Unity IAP Initialization

async void InitializePurchasing()
{
// Create store controller instance
storeController = UnityIAPServices.StoreController();

// Subscribe to purchase events
storeController.OnStoreDisconnected += OnStoreDisconnected;
storeController.OnProductsFetched += OnProductsFetched;
storeController.OnProductsFetchFailed += OnProductsFetchFailed;
storeController.OnPurchasePending += OnPurchasePending;
storeController.OnPurchaseConfirmed += OnPurchaseConfirmed;
storeController.OnPurchaseFailed += OnPurchaseFailed;
storeController.OnPurchaseDeferred += OnPurchaseDeferred;

// Connect to store (async operation)
await storeController.Connect();

// Fetch product information
FetchProducts();
}

void FetchProducts()
{
// Define products to fetch from App Store
var products = new List<ProductDefinition>
{
new(COINS, ProductType.Consumable),
new(REMOVE_ADS, ProductType.NonConsumable),
new(VIP, ProductType.Subscription),
new(EVENT_PASS, ProductType.Subscription)
};

storeController.FetchProducts(products);
}

Important:

  • await storeController.Connect() is asynchronous - make method async void

  • Product types must match App Store configuration

  • FetchProducts() retrieves localized pricing and metadata from Apple

Step 4: Purchase Flow Implementation

Initiating a Purchase

public void Buy(string productId)
{
if (storeController == null)
{
Debug.LogError("IAP not initialized");
return;
}

// Trigger purchase flow
storeController.PurchaseProduct(productId);
}

Usage example:

// Call from UI button
Buy(COINS);

Handling Purchase Pending

void OnPurchasePending(PendingOrder order)
{
var product = GetFirstProductInOrder(order);
if (product == null)
{
Debug.LogError("Product not found in pending order");
return;
}

// Extract receipt (JWS for StoreKit 2)
lastReceipt = ExtractReceipt(order);

// Extract transaction ID (StoreKit 2 specific)
if (!string.IsNullOrEmpty(order.Info.TransactionID))
{
lastTransactionId = order.Info.TransactionID;
}

Debug.Log($"Purchase pending: {product.definition.id}");
Debug.Log($"Transaction ID: {lastTransactionId}");

// Confirm purchase (moves to OnPurchaseConfirmed)
storeController.ConfirmPurchase(order);
}

StoreKit 2 Key Feature:

  • order.Info.TransactionID provides unique transaction identifier

  • This ID is used for brainCloud verification

  • Receipt contains JWS signature instead of legacy base64

Extracting Receipt Data

static string ExtractReceipt(Order order)
{
// Try StoreKit 2 JWS first
var jws = order.Info.Apple?.jwsRepresentation;
if (!string.IsNullOrEmpty(jws))
{
return jws;
}

// Fallback to legacy receipt
return order.Info.Receipt;
}

Step 5: Purchase Confirmation

void OnPurchaseConfirmed(Order order)
{
switch (order)
{
case ConfirmedOrder confirmedOrder:
{
var product = GetFirstProductInOrder(confirmedOrder);

// Store transaction ID
if (!string.IsNullOrEmpty(confirmedOrder.Info.TransactionID))
{
lastTransactionId = confirmedOrder.Info.TransactionID;
}

Debug.Log($"Purchase confirmed: {product?.definition.id}");
Debug.Log($"Transaction ID: {lastTransactionId}");

// Grant content to player here
GrantPurchaseContent(product);

break;
}
case FailedOrder failedOrder:
{
var product = GetFirstProductInOrder(failedOrder);
Debug.LogError($"Confirmation failed: {product?.definition.id}");
Debug.LogError($"Reason: {failedOrder.FailureReason}");
break;
}
}
}

void GrantPurchaseContent(Product product)
{
// Implement your content granting logic
switch (product.definition.id)
{
case COINS:
// Add 100 coins to player
Debug.Log("Granted 100 coins");
break;
case REMOVE_ADS:
// Disable ads permanently
Debug.Log("Ads removed");
break;
case VIP:
// Activate VIP subscription
Debug.Log("VIP activated");
break;
case EVENT_PASS:
// Activate event pass
Debug.Log("Event pass activated");
break;
}
}

Step 6: brainCloud Receipt Verification

Authentication First

void AuthenticateBrainCloud()
{
if (bc == null)
{
Debug.LogError("brainCloud not initialized");
return;
}

bc.AuthenticateUniversal(
"user_unique_id", // User ID (can be device ID, custom ID, etc.)
"user_password", // Password (or generate random for anonymous)
true, // Create account if doesn't exist

// Success callback
(json, cb) =>
{
Debug.Log("brainCloud authenticated successfully");
Debug.Log($"Response: {json}");
},

// Error callback
(status, reason, error, cb) =>
{
Debug.LogError($"brainCloud auth failed: {reason}");
Debug.LogError($"Error: {error}");
}
);
}

Call before verifying purchases:

// Authenticate once at app start or before first purchase
AuthenticateBrainCloud();

Verify Purchase with Transaction ID (Recommended for StoreKit 2)

void VerifyLastPurchase()
{
if (bc == null || !bc.Client.Authenticated)
{
Debug.LogError("brainCloud not authenticated");
return;
}

// Prefer transaction ID verification (StoreKit 2)
if (!string.IsNullOrEmpty(lastTransactionId))
{
Debug.Log("Verifying with StoreKit 2 transaction ID...");

string storeId = "itunes";

// Format receipt data with transaction ID
string receiptData = $"{{\"transactionId\":\"{lastTransactionId}\",\"excludeOldTransactions\":false}}";

bc.AppStoreService.VerifyPurchase(
storeId,
receiptData,

// Success callback
(json, cb) =>
{
Debug.Log("✅ Purchase verified successfully!");
Debug.Log($"Response: {json}");

// Parse response and grant rewards server-side if needed
HandleVerificationSuccess(json);
},

// Error callback
(status, reason, error, cb) =>
{
Debug.LogError($"❌ Verification failed: {reason}");
Debug.LogError($"Error details: {error}");

// Handle verification failure
HandleVerificationFailure(reason, error);
}
);

return;
}

// Fallback: Verify with full receipt (legacy/StoreKit 1)
if (!string.IsNullOrEmpty(lastReceipt))
{
Debug.Log("Verifying with receipt data...");

string storeId = "itunes";

bc.AppStoreService.VerifyPurchase(
storeId,
lastReceipt, // Full JWS or legacy receipt
(json, cb) =>
{
Debug.Log("✅ Purchase verified!");
Debug.Log(json);
},
(status, reason, error, cb) =>
{
Debug.LogError($"❌ Verification failed: {reason}");
Debug.LogError(error);
}
);

return;
}

Debug.LogError("No receipt or transaction ID available");
}

Handling Verification Response

void HandleVerificationSuccess(string jsonResponse)
{
// Parse brainCloud response
// Response contains:
// - Transaction details
// - Product information
// - Verification status

Debug.Log("Purchase verified by brainCloud server");

// You can now safely grant premium content
// brainCloud will prevent duplicate grants automatically
}

void HandleVerificationFailure(string reason, string error)
{
// Handle different failure scenarios
if (reason.Contains("duplicate"))
{
Debug.Log("Purchase already verified (duplicate)");
// Already granted, no action needed
}
else if (reason.Contains("invalid"))
{
Debug.LogError("Invalid receipt - possible fraud attempt");
// Do NOT grant content
}
else
{
Debug.LogError("Verification error - retry later");
// Queue for retry
}
}

Step 7: Error Handling

void OnPurchaseFailed(FailedOrder order)
{
var product = GetFirstProductInOrder(order);

// Handle duplicate transactions gracefully
if (order.FailureReason == PurchaseFailureReason.DuplicateTransaction)
{
Debug.LogWarning($"Duplicate transaction: {product?.definition.id}");
// Already processed, ignore
return;
}

Debug.LogError($"Purchase failed: {product?.definition.id}");
Debug.LogError($"Reason: {order.FailureReason}");
Debug.LogError($"Details: {order.Details}");

// Show user-friendly error message
ShowErrorToUser($"Purchase failed: {order.FailureReason}");
}

void OnPurchaseDeferred(DeferredOrder order)
{
var product = GetFirstProductInOrder(order);
Debug.LogWarning($"Purchase deferred (requires parental approval): {product?.definition.id}");

// Inform user to check with parent/guardian
ShowMessageToUser("Purchase requires approval. Please check later.");
}

void OnStoreDisconnected(StoreConnectionFailureDescription description)
{
Debug.LogError($"Store connection failed: {description.message}");

// Attempt reconnection or notify user
ShowErrorToUser("Cannot connect to App Store. Please check internet connection.");
}

Step 8: Utility Helper Methods

// Extract first product from order
static Product GetFirstProductInOrder(Order order)
{
return order.CartOrdered.Items().FirstOrDefault()?.Product;
}

// Detailed logging for debugging
void LogOrderDetails(string stage, Order order, Product product)
{
var info = order.Info;
var jws = info.Apple?.jwsRepresentation;
var storeName = info.Apple?.StoreName ?? "Unknown";

Debug.Log($" Order {stage}:");
Debug.Log($" Product: {product?.definition.id}");
Debug.Log($" Type: {product?.definition.type}");
Debug.Log($" Transaction ID: {info.TransactionID}");
Debug.Log($" Store: {storeName}");
Debug.Log($" Has Receipt: {!string.IsNullOrEmpty(info.Receipt)}");
Debug.Log($" Has JWS: {!string.IsNullOrEmpty(jws)}");
}

Test and monitor the log from brainCloud

  • Use your sandbox tester email when testing purchases.

  • Review the results in the user logs on the brainCloud portal.

  • And transaction details from the User Transactions page

Did this answer your question?