In-App Purchases Integration (Unity Plugin)
The iOS Billing SDK is a simple solution to implement Aptoide billing. Its Unity Plugin provides a simple interface for Unity games to communicate with the SDK. It consists of a Billing client that allows you to get your products from Aptoide Connect and process the purchase of those items.
In Summary
The billing flow in your application with the Plugin is as follows:
- Add the AppCoins Unity Plugin;
- Query your In-App Products;
- User wants to purchase a product;
- Application starts the purchase and the Plugin handles it, returning the purchase status and validation data on completion;
- Application gives the product to the user.
Requirements
- Unity 2019.4 or later.
- iOS 17.4 or higher.
Step-by-Step Guide
Setup
- Add AppCoins Unity Plugin
In Unity, add the plugin from the latest release available on the repository https://github.com/Catappult/appcoins-sdk-ios-unity-plugin to your Assets folder.
Implementation
Now that you have the Plugin set-up you can start making use of its functionalities.
-
Check AppCoins Billing Availability
The AppCoins Billing will only be available on devices with an iOS version equal to or higher than 17.4 and only if the application was not installed through the Apple App Store. Therefore, before attempting any purchase, you should check if the SDK is available by calling
AppCoinsSDK.Instance.IsAvailable().var isAvailable = await AppCoinsSDK.Instance.IsAvailable(); if (isAvailable) { // make purchase } -
Query In-App Products
You should start by getting the In-App Products you want to make available to the user. This method can either return all of your Catappult In-App Products or a specific list.
-
AppCoinsSDK.Instance.GetProducts()Returns all application Catappult In-App Products:
var productsResult = await AppCoinsSDK.Instance.GetProducts(); if (productsResult.IsSuccess) { var products = productsResult.Value; // Process products } else { Debug.Log("Error: " + productsResult.Error); } -
AppCoinsSDK.Instance.GetProducts(skus)Returns a specific list of Catappult In-App Products:
var productsResult = await AppCoinsSDK.Instance.GetProducts(new string[] { "coins_100", "gas" }); if (productsResult.IsSuccess) { var products = productsResult.Value; // Process products } else { Debug.Log("Error: " + productsResult.Error); }Warning: You will only be able to query your In-App Products once your application is reviewed and approved on Aptoide Connect.
-
-
Purchase In-App Product
To purchase an In-App Product you must call the function
AppCoinsSDK.Instance.Purchase(sku, payload). The Plugin will handle all of the purchase logic for you and it will return you on completion the result of the purchase. This result is anAppCoinsSDKPurchaseResultobject with the following properties:State: String - The purchase state (AppCoinsSDK.PURCHASE_STATE_SUCCESS,AppCoinsSDK.PURCHASE_STATE_PENDING,AppCoinsSDK.PURCHASE_STATE_USER_CANCELLED,AppCoinsSDK.PURCHASE_STATE_FAILED)Value: Object containing:VerificationResult: String - The verification result (AppCoinsSDK.PURCHASE_VERIFICATION_STATE_VERIFIED,AppCoinsSDK.PURCHASE_VERIFICATION_STATE_UNVERIFIED)Purchase: Purchase objectVerificationError: AppCoinsSDKError (only present if verification fails)
Error: AppCoinsSDKError - Error details (only present if state isFAILED)
In case of success the application will verify the transaction's signature locally. After this verification you should handle its result:
- If the purchase is verified you should consume the item and give it to the user.
- If it is not verified you need to make a decision based on your business logic, you either still consume the item and give it to the user, or otherwise the purchase will not be acknowledged and we will refund the user in 24 hours.
In case of failure you can deal with different types of errors.
You can also pass a Payload to the purchase method in order to associate some sort of information with a specific purchase. You can use this for example to associate a specific user with a Purchase:
AppCoinsSDK.Instance.Purchase("gas", "User123").
var purchaseResult = await AppCoinsSDK.Instance.Purchase("gas", "User123"); switch (purchaseResult.State) { case AppCoinsSDK.PURCHASE_STATE_SUCCESS: switch (purchaseResult.Value.VerificationResult) { case AppCoinsSDK.PURCHASE_VERIFICATION_STATE_VERIFIED: // Consume the item and give it to the user var consumeResult = await AppCoinsSDK.Instance.ConsumePurchase(purchaseResult.Value.Purchase.Sku); if (consumeResult.IsSuccess) { Debug.Log("Purchase consumed successfully"); } else { Debug.Log("Error consuming purchase: " + consumeResult.Error); } break; case AppCoinsSDK.PURCHASE_VERIFICATION_STATE_UNVERIFIED: // Handle unverified purchase according to your game logic break; } break; case AppCoinsSDK.PURCHASE_STATE_PENDING: // Handle pending purchase according to your game logic Debug.Log("Purchase is pending."); break; case AppCoinsSDK.PURCHASE_STATE_USER_CANCELLED: // Handle cancelled purchase according to your game logic Debug.Log("Purchase was cancelled."); break; case AppCoinsSDK.PURCHASE_STATE_FAILED: // Handle failed purchase according to your game logic Debug.Log("Purchase failed with error: " + purchaseResult.Error); break; } -
Handle Unfinished Purchases on App Launch (CRITICAL)
CRITICAL: You MUST query and consume unfinished purchases every time your application starts. Failing to do so will result in users not receiving items they've already paid for, and purchases will be automatically refunded after 24 hours if not consumed.
What are Unfinished Purchases?
Unfinished purchases are transactions that have been paid for but not yet consumed by your application. This can happen if:
- The app was closed or crashed during a purchase
- The user force-quit the app before the purchase was processed
- A network error occurred during purchase completion
Why This is Critical:
- Users have already paid for these items
- If not consumed within 24 hours, purchases are automatically refunded
- Users expect to receive their purchased items immediately upon reopening the app
Implementation:
Add this code to your application's startup logic (e.g., in your main scene's
Start()orAwake()method):private async void Start() { // Check if AppCoins Billing is available var isAvailable = await AppCoinsSDK.Instance.IsAvailable(); if (!isAvailable) { return; } // Query and consume unfinished purchases var unfinishedPurchasesResult = await AppCoinsSDK.Instance.GetUnfinishedPurchases(); if (unfinishedPurchasesResult.IsSuccess) { var purchases = unfinishedPurchasesResult.Value; foreach (var purchase in purchases) { // Give the item to the user GiveItemToUser(purchase.Sku); // Consume the purchase var consumeResult = await AppCoinsSDK.Instance.ConsumePurchase(purchase.Sku); if (consumeResult.IsSuccess) { Debug.Log($"Unfinished purchase consumed successfully: {purchase.Sku}"); } else { Debug.Log($"Error consuming purchase: {consumeResult.Error}"); } } } else { Debug.Log("Error querying unfinished purchases: " + unfinishedPurchasesResult.Error); } } private void GiveItemToUser(string sku) { // Your logic to grant the purchased item to the user Debug.Log($"Giving item to user: {sku}"); } -
Handle Indirect Purchases
In addition to standard In-App Purchases, the AppCoins SDK supports In-App Purchase Intents – purchases not directly triggered by a user action (e.g., tapping a "Buy" button within the app). Common use cases include:
- Purchasing an item directly from a catalog of In-App Products in the Aptoide Store.
- Buying an item through a web link.
Purchase Intents can be initiated through the following URL format:
The
AppCoinsPurchaseManager.OnPurchaseUpdatedUnity Action allows developers to manage these purchase intents. This event continuously streams purchase intent updates, ensuring real-time transaction synchronization.The event returns a
PurchaseIntentobject containing:ID: String - Unique identifier for the intentProduct: Product - The product the user wants to purchaseTimestamp: String - When the intent was created
When you receive a
PurchaseIntent, you must either confirm it usingAppCoinsSDK.Instance.ConfirmPurchaseIntent(payload)to complete the purchase, or reject it usingAppCoinsSDK.Instance.RejectPurchaseIntent()to cancel. Confirming the intent returns anAppCoinsSDKPurchaseResultthat should be handled the same way as a standard purchase.To properly handle purchase intents, subscribe to the event within a singleton class, ensuring it remains active for the application's lifecycle.
Note: You can also manually check for pending purchase intents using
AppCoinsSDK.Instance.GetPurchaseIntent(). This is useful when the user signs in or when your app becomes active, to ensure no pending intents are missed.
private void Awake() { // Singleton enforcement if (Instance != null && Instance != this) { Destroy(gameObject); // Destroy duplicate instances return; } Instance = this; DontDestroyOnLoad(gameObject); // Persist across scenes // Subscribe to purchase intent updates AppCoinsPurchaseManager.OnPurchaseUpdated += HandlePurchaseIntent; } private async void HandlePurchaseIntent(PurchaseIntent purchaseIntent) { Debug.Log($"Received purchase intent for: {purchaseIntent.Product.Title}"); // Confirm the purchase intent to complete the transaction var purchaseResult = await AppCoinsSDK.Instance.ConfirmPurchaseIntent("User123"); // Handle the purchase result the same way as a standard purchase switch (purchaseResult.State) { case AppCoinsSDK.PURCHASE_STATE_SUCCESS: switch (purchaseResult.Value.VerificationResult) { case AppCoinsSDK.PURCHASE_VERIFICATION_STATE_VERIFIED: var consumeResult = await AppCoinsSDK.Instance.ConsumePurchase(purchaseResult.Value.Purchase.Sku); if (consumeResult.IsSuccess) { Debug.Log("Purchase consumed successfully"); } else { Debug.Log("Error consuming purchase: " + consumeResult.Error); } break; case AppCoinsSDK.PURCHASE_VERIFICATION_STATE_UNVERIFIED: // Handle unverified purchase according to your game logic break; } break; case AppCoinsSDK.PURCHASE_STATE_USER_CANCELLED: Debug.Log("Purchase was cancelled."); break; case AppCoinsSDK.PURCHASE_STATE_FAILED: Debug.Log("Purchase failed with error: " + purchaseResult.Error); break; } // Alternatively, reject the purchase intent to cancel: // AppCoinsSDK.Instance.RejectPurchaseIntent(); } -
Query Purchases
You can query the user's purchases by using one of the following methods:
-
AppCoinsSDK.Instance.GetAllPurchases()This method returns all purchases that the user has performed in your application.
var purchasesResult = await AppCoinsSDK.Instance.GetAllPurchases(); if (purchasesResult.IsSuccess) { var purchases = purchasesResult.Value; // Process purchases } else { Debug.Log("Error: " + purchasesResult.Error); } -
AppCoinsSDK.Instance.GetLatestPurchase(string sku)This method returns the latest user purchase for a specific In-App Product. Returns
nullif no purchase is found.var latestPurchaseResult = await AppCoinsSDK.Instance.GetLatestPurchase("gas"); if (latestPurchaseResult.IsSuccess) { if (latestPurchaseResult.Value != null) { var purchase = latestPurchaseResult.Value; // Process purchase } else { Debug.Log("No latest purchase found for this SKU"); } } else { Debug.Log("Error: " + latestPurchaseResult.Error); } -
AppCoinsSDK.Instance.GetUnfinishedPurchases()This method returns all of the user's unfinished purchases in the application. An unfinished purchase is any purchase that has neither been acknowledged (verified by the SDK) nor consumed. You can use this method for consuming any unfinished purchases.
var unfinishedPurchasesResult = await AppCoinsSDK.Instance.GetUnfinishedPurchases(); if (unfinishedPurchasesResult.IsSuccess) { var purchases = unfinishedPurchasesResult.Value; foreach (var purchase in purchases) { var consumeResult = await AppCoinsSDK.Instance.ConsumePurchase(purchase.Sku); if (consumeResult.IsSuccess) { Debug.Log("Unfinished purchase consumed successfully"); } else { Debug.Log("Error consuming purchase: " + consumeResult.Error); } } } else { Debug.Log("Error: " + unfinishedPurchasesResult.Error); }
-
Testing
To test the SDK integration during development, you'll need to set the installation source for development builds, simulating that the app is being distributed through Aptoide. This action will enable the SDK's isAvailable method.
Follow these steps in Xcode:
-
In your target build settings, search for "Marketplaces".
-
Under "Deployment", set the key "Marketplaces" or "Alternative Distribution - Marketplaces" to "com.aptoide.ios.store".
-
In your scheme, go to the "Run" tab, then navigate to the "Options" tab. In the "Distribution" dropdown, select "com.aptoide.ios.store".
For more information, please refer to Apple's official documentation: https://developer.apple.com/documentation/appdistribution/distributing-your-app-on-an-alternative-marketplace#Test-your-app-during-development
Testing Both Billing Systems in One Build
To facilitate testing both Apple Billing and Aptoide Billing within a single build – without the need to generate separate versions of your application – the AppCoins SDK includes a deep link mechanism that toggles the SDK's isAvailable method between true and false. This allows you to seamlessly switch between testing the AppCoins SDK (when available) and Apple Billing (when unavailable).
To enable or disable the AppCoins SDK, open your device's browser and enter the following URL:
{domain}.iap://wallet.appcoins.io/default?value={value}Where:
domain– The Bundle ID of your application.valuetrue→ Enables the AppCoins SDK for testing.false→ Disables the AppCoins SDK, allowing Apple Billing to be tested instead.
Sandbox
To verify the successful setup of your billing integration, we offer a sandbox environment where you can simulate purchases and ensure that your clients can smoothly purchase your products. Documentation on how to use this environment can be found at: Sandbox
Classes Definition and Properties
The Unity Plugin integration is based on several main classes of objects that handle its logic:
Product
Product represents an in-app product.
Properties:
Sku: String - Unique product identifier. Example: gasTitle: String - The product display title. Example: Best GasDescription: String - The product description. Example: Buy gas to fill the tank.PriceCurrency: String - The user's geolocalized currency. Example: EURPriceValue: String - The value of the product in the specified currency. Example: 0.93PriceLabel: String - The label of the price displayed to the user. Example: €0.93PriceSymbol: String - The symbol of the geolocalized currency. Example: €
Purchase
Purchase represents an in-app purchase.
Properties:
UID: String - Unique purchase identifier. Example: catappult.inapp.purchase.ABCDEFGHIJ1234Sku: String - Unique identifier for the product that was purchased. Example: gasState: String - The purchase state can be one of three: PENDING, ACKNOWLEDGED, and CONSUMED. Pending purchases are purchases that have neither been verified by the SDK nor have been consumed by the application. Acknowledged purchases are purchases that have been verified by the SDK but have not been consumed yet. Example: CONSUMEDOrderUID: String - The orderUid associated with the purchase. Example: ZWYXGYZCPWHZDZUK4HPayload: String - The developer Payload. Example: 707048467.998992Created: String - The creation date for the purchase. Example: 2023-01-01T10:21:29.014456ZVerification: PurchaseVerification - The verification data associated with the purchase.
PurchaseVerification
PurchaseVerification represents an in-app purchase verification data.
Properties:
Type: String - The type of verification made. Example: GOOGLESignature: String - The purchase signature. Example: C4x6cr0HJk0KkRqJXUrRAhdANespHEsyx6ajRjbG5G/v3uBzlthkUe8BO7NXH/1Yi/UhS5sk7huA+hB8EbaQK9bwaiV/Z3dISl5jgYqzSEz1c/PFPwVEHZTMrdU07i/q4FD33x0LZIxrv2XYbAcyNVRY3GLJpgzAB8NvKtumbWrbV6XG4gBmYl9w4oUgJLnedii02beKlvmR7suQcqIqlSKA9WEH2s7sCxB5+kYwjQ5oHttmOQENnJXlFRBQrhW89bl18rccF05ur71wNOU6KgMcwppUccvIfXUpDFKhXQs4Ut6c492/GX1+KzbhotDmxSLQb6aw6/l/kzaSxNyjHg==Data: PurchaseVerificationData - The data associated with the verification of the purchase.
PurchaseVerificationData
PurchaseVerificationData represents the body of an in-app purchase verification data.
Properties:
OrderId: String - The orderUid associated with the purchase. Example: 372EXWQFTVMKS6HIPackageName: String - Bundle ID of the product's application. Example: com.appcoins.trivialdrivesampleProductId: String - Unique identifier for the product that was purchased. Example: gasPurchaseTime: Integer - The time the product was purchased. Example: 1583058465823PurchaseToken: String - The token provided to the user's device when the product was purchased. Example: catappult.inapp.purchase.SZYJ5ZRWUATW5YU2PurchaseState: Integer - The purchase state of the order. Possible values are: 0 (Purchased) and 1 (Canceled)DeveloperPayload: String - A developer-specified string that contains supplemental information about an order. Example: myOrderId:12345678
PurchaseIntent
PurchaseIntent represents an indirect in-app purchase intent.
Properties:
ID: String - Unique identifier for the intent. Example: 550e8400-e29b-41d4-a716-446655440000Product: Product - The product the user wants to purchaseTimestamp: String - When the intent was created. Example: 2025-01-15T10:21:29.014456Z
AppCoinsSDKPurchaseResult
AppCoinsSDKPurchaseResult represents the result of a purchase operation.
Properties:
State: String - The purchase state. Can be:AppCoinsSDK.PURCHASE_STATE_SUCCESS- Purchase completed successfullyAppCoinsSDK.PURCHASE_STATE_PENDING- Purchase is pendingAppCoinsSDK.PURCHASE_STATE_USER_CANCELLED- User cancelled the purchaseAppCoinsSDK.PURCHASE_STATE_FAILED- Purchase failed
Value: Object (only present when State is SUCCESS) containing:VerificationResult: String - Can beAppCoinsSDK.PURCHASE_VERIFICATION_STATE_VERIFIEDorAppCoinsSDK.PURCHASE_VERIFICATION_STATE_UNVERIFIEDPurchase: Purchase - The purchase objectVerificationError: AppCoinsSDKError (optional) - Error details if verification failed
Error: AppCoinsSDKError (only present when State is FAILED) - Error details
AppCoinsSDKResult<T>
AppCoinsSDKResult<T> represents the result of SDK operations that return data.
Properties:
IsSuccess: Boolean - Whether the operation succeededValue: T - The result value (only present when IsSuccess is true)Error: AppCoinsSDKError (only present when IsSuccess is false) - Error details
Used by:
GetProducts()- ReturnsAppCoinsSDKResult<Product[]>GetAllPurchases()- ReturnsAppCoinsSDKResult<Purchase[]>GetLatestPurchase(sku)- ReturnsAppCoinsSDKResult<Purchase>GetUnfinishedPurchases()- ReturnsAppCoinsSDKResult<Purchase[]>ConsumePurchase(sku)- ReturnsAppCoinsSDKResult<bool>GetTestingWalletAddress()- ReturnsAppCoinsSDKResult<string>GetPurchaseIntent()- ReturnsAppCoinsSDKResult<PurchaseIntent>
AppCoinsSDKError
AppCoinsSDKError represents error information when an SDK operation fails.
Properties:
Type: String - The error type. Can be:networkError- Network-related errorsystemError- System or SDK errornotEntitled- User is not entitled to the productproductUnavailable- Product is not availablepurchaseNotAllowed- Purchase is not allowedunknown- Unknown error
Message: String - A brief error messageDescription: String - A detailed error descriptionRequest: ErrorRequest (optional) - Request details if available
ErrorRequest
Properties:
URL: String - The request URLMethod: String - The HTTP methodBody: String - The request bodyResponseData: String - The response dataStatusCode: Integer - The HTTP status code
PurchaseIntent represents a user's intent to make an in-app purchase. It is typically used to confirm or reject a purchase initiated outside the application.
This class is responsible for general purpose methods and provides singleton access via AppCoinsSDK.Instance.
Updated 20 days ago
