Skip to main content

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.

ConditionAppCoins billing active?
iOS earlier than 17.4No
iOS 17.4+, installed from the Apple App StoreNo
iOS 17.4+, installed via TestFlightNo
iOS 17.4+, installed via Aptoide or another alternative marketplaceYes
iOS 17.4+, web distribution or other non-App Store sourceYes
iOS 17.4+, SimulatorYes (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:

ActionDeep 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:

  1. 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).
  2. Add Keychain Sharing, URL Scheme, and MKSellsDigitalGoods as described in the integration guide.
  3. Replace your billing implementation file(s) with AppCoins API calls (see Swift and Unity diffs below).
  4. Add AppcSDK.initialize() and AppcSDK.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.

🚧
Do not commit platform-specific changes directly to 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 2AppCoinsSDK
import StoreKitimport AppCoinsSDK
StoreKit.Product.products(for:)Product.products(for:)
product.purchase()product.purchase()
transaction.finish()transaction.finish()
Transaction.unfinishedTransaction.unfinished
transaction.id: UInt64transaction.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:

  1. In your target's build settings, search for Marketplaces.
  2. Under Deployment, set the value to com.aptoide.ios.store.
  3. 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 BuildSeparate Builds
CodebaseOne branch, one submission per storeTwo branches, one submission per store
AppCoins code in App Store binaryYes (dormant when unavailable)No
Maintenance overheadLower — rebase not requiredLow — diff is small, rebase is fast
Fallback to Apple billingAutomatic at runtimeNot applicable — each build targets one store
Best forMost teamsTeams with strict binary hygiene requirements

For the majority of apps, the single build strategy is the simpler choice.


Next steps