Integration Strategies
There are two ways to structure AppCoins billing in your app: ship one binary that detects the distribution channel at runtime, or maintain separate binaries per store. If you are distributing exclusively on Aptoide with no Apple App Store presence, use the separate-builds approach — there is no Apple billing path to maintain.
Single Build
One binary ships to both stores. At runtime the SDK detects whether the app was installed from Aptoide or the Apple App Store and enables or disables AppCoins billing accordingly. If the SDK is unavailable the code falls through to your Apple billing path.
When to use: You want one submission per store, one codebase to maintain, and automatic fallback to Apple billing with no extra configuration.
How runtime detection works
On iOS 17.4 and later, the SDK calls AppDistributor.current from Apple's MarketplaceKit to determine how the app was installed. The result drives AppcSDK.isAvailable() in Swift and the AppCoinsStoreMode.Automatic selection in Unity.
| Condition | AppCoins billing active? |
|---|---|
| iOS earlier than 17.4 | No |
| iOS 17.4+, installed from the Apple App Store | No |
| iOS 17.4+, installed via TestFlight | No |
| iOS 17.4+, installed via Aptoide or another alternative marketplace | Yes |
| iOS 17.4+, web distribution or other non-App Store source | Yes |
| iOS 17.4+, Simulator | Yes (AppDistributor cannot be queried in the Simulator — use a real device to test the Apple billing fallback) |
AppCoins activates on iOS 17.4+ for any install source that is not the Apple App Store and not TestFlight.
Swift example
import AppCoinsSDK
import StoreKit
func purchase(sku: String) async {
if await AppcSDK.isAvailable() {
// AppCoins billing path
let products = try? await Product.products(for: [sku])
guard let product = products?.first else { return }
let result = try? await product.purchase()
switch result {
case .success(let verificationResult):
if case .verified(let transaction) = verificationResult {
grantItem(sku: transaction.productID)
await transaction.finish()
}
case .pending, .userCancelled, .none:
break
}
} else {
// Apple billing path — standard StoreKit 2
let products = try? await StoreKit.Product.products(for: [sku])
guard let product = products?.first else { return }
let result = try? await product.purchase()
switch result {
case .success(let verificationResult):
if case .verified(let transaction) = verificationResult {
grantItem(sku: transaction.productID)
await transaction.finish()
}
case .pending, .userCancelled, .none:
break
}
}
}
Unity (C#) example
Use AppCoinsStoreMode.Automatic — it detects availability at runtime and routes to AppCoins or Apple billing transparently. Your OnPurchasePending handler works identically for both stores.
using AppCoins.Unity;
using UnityEngine.Purchasing;
private IStoreController _controller;
private async void Start()
{
// Automatic mode: uses AppCoins on Aptoide installs, Apple elsewhere
await AppCoinsIAP.ConfigureStoreAsync(AppCoinsStoreMode.Automatic);
_controller = UnityIAPServices.StoreController();
_controller.OnProductsFetched += OnProductsFetched;
_controller.OnPurchasePending += OnPurchasePending;
_controller.OnPurchaseFailed += OnPurchaseFailed;
await _controller.Connect();
}
private void OnProductsFetched(Products products)
{
// products available — populate your shop UI
}
private void OnPurchasePending(PendingOrder order)
{
// Same handler regardless of which store processed the purchase
GiveItemToUser(order.CartOrdered.Items[0].Product.definition.id);
_controller.ConfirmPurchase(order);
}
private void OnPurchaseFailed(FailedOrder order)
{
Debug.Log($"Purchase failed: {order.FailureReason}");
}
Testing the single build
Use these deep links from Safari to switch between billing paths on a single device without rebuilding:
| Action | Deep link |
|---|---|
| Force AppCoins billing | {bundle_id}.iap://wallet.appcoins.io/default/mode?value=appcoins |
| Force Apple billing | {bundle_id}.iap://wallet.appcoins.io/default/mode?value=apple |
| Restore automatic detection | {bundle_id}.iap://wallet.appcoins.io/default/mode?value=automatic |
Replace {bundle_id} with your app's bundle identifier. Open the URL in Safari on the device. The change persists until toggled again.
Mode overrides have no effect on builds installed from the Apple App Store, to prevent misuse.
Separate Builds
Two binaries: the main branch stays unchanged and ships to the Apple App Store. A separate aptoide branch replaces the billing implementation with AppCoins and ships to Aptoide.
When to use: You want a completely clean binary for each store — no AppCoins code in the App Store build, and no StoreKit dependency in the Aptoide build. You also want explicit control over what goes into each submission.
Creating the aptoide branch
git checkout -b aptoide
On the aptoide branch:
- Add the AppCoinsSDK Swift Package dependency (SPM URL:
https://github.com/Catappult/appcoins-sdk-ios.git). When prompted for a version rule, select Up to Next Major Version starting from the latest major version (e.g.5.0.0). - Add Keychain Sharing, URL Scheme, and
MKSellsDigitalGoodsas described in the integration guide. - Replace your billing implementation file(s) with AppCoins API calls (see Swift and Unity diffs below).
- Add
AppcSDK.initialize()andAppcSDK.handle(redirectURL:)to your entry points.
Maintaining the aptoide branch
Develop only on main. Before cutting an Aptoide build, rebase the aptoide branch onto the latest main:
git checkout aptoide
git rebase main
Because the AppCoins diff is limited to 1–2 billing files, conflicts are rare. The rebase keeps the branch current without a merge commit.
aptoide. Develop on main first, then rebase. This keeps the diff small and predictable.Swift: what the diff looks like
The AppCoins Swift API mirrors StoreKit 2 intentionally. The changes are minimal:
| StoreKit 2 | AppCoinsSDK |
|---|---|
import StoreKit | import AppCoinsSDK |
StoreKit.Product.products(for:) | Product.products(for:) |
product.purchase() | product.purchase() |
transaction.finish() | transaction.finish() |
Transaction.unfinished | Transaction.unfinished |
transaction.id: UInt64 | transaction.id: String |
The only non-obvious difference is Transaction.id, which is a String in AppCoinsSDK (not a UInt64 as in StoreKit 2). If your code stores or compares transaction IDs, update those usages.
Unity: what the diff looks like
The diff on the aptoide branch is one line: change AppCoinsStoreMode.Automatic to AppCoinsStoreMode.AppCoins. Everything else — Connect(), OnPurchasePending, ConfirmPurchase — stays identical.
// main branch (Apple App Store):
await AppCoinsIAP.ConfigureStoreAsync(AppCoinsStoreMode.Apple);
// aptoide branch — only this line changes:
await AppCoinsIAP.ConfigureStoreAsync(AppCoinsStoreMode.AppCoins);
The rest of your Unity IAP v5 setup (Connect, event handlers, InitiatePurchase, ConfirmPurchase) is unchanged between branches. See the Unity integration guide for the full setup.
Testing the separate builds
To test the aptoide branch build on device, set the Marketplaces build setting in Xcode:
- In your target's build settings, search for Marketplaces.
- Under Deployment, set the value to
com.aptoide.ios.store. - In your scheme's Run → Options, set Distribution to
com.aptoide.ios.store.
This simulates an Aptoide-distributed install so AppcSDK.isAvailable() returns true during development.
Decision Guide
| Single Build | Separate Builds | |
|---|---|---|
| Codebase | One branch, one submission per store | Two branches, one submission per store |
| AppCoins code in App Store binary | Yes (dormant when unavailable) | No |
| Maintenance overhead | Lower — rebase not required | Low — diff is small, rebase is fast |
| Fallback to Apple billing | Automatic at runtime | Not applicable — each build targets one store |
| Best for | Most teams | Teams with strict binary hygiene requirements |
For the majority of apps, the single build strategy is the simpler choice.