Skip to main content

Adding AppCoins alongside StoreKit 2

This guide covers adding AppCoins billing to an app that uses StoreKit 2. The two APIs are intentionally near-identical. In most cases, three changes are required: the import, the SDK initialization call, and one type difference on transaction.id.

Overview

The AppCoins SDK mirrors the StoreKit 2 public interface by design. Product.products(for:), product.purchase(options:), Transaction.unfinished, transaction.finish(), and VerificationResult all exist in both SDKs with the same signatures.

The AppCoins SDK activates based on the iOS version and how the app was distributed — see How Runtime Detection Works.

See iOS Billing Integration Strategies to decide which approach fits your setup. This guide covers both.

Xcode Configuration

Three settings are required on your Xcode target before the SDK can operate:

  1. Keychain Sharing

    1. Select your project in the Project Navigator (left sidebar).
    2. Select your target under TARGETS.
    3. Go to the Signing & Capabilities tab.
    4. Click the + button to add a new capability.
    5. Search for Keychain Sharing and select it.
    6. In the Keychain Groups field, replace the default value with exactly com.aptoide.appcoins-wallet.
  2. URL Scheme

    1. Select your target under TARGETS.
    2. Navigate to the Info tab.
    3. Expand the URL Types section and click +.
    4. Set URL Schemes to $(PRODUCT_BUNDLE_IDENTIFIER).iap and Role to Editor.
  3. MKSellsDigitalGoods

    1. In the Info tab, scroll to the Custom iOS Target Properties section and click +.
    2. Add the key MKSellsDigitalGoods and set its value to YES (Boolean).
⚠️
All three settings are required for AppCoins billing to function. Missing any of them will prevent purchases from being processed.

The 3 Changes

1. Import and Initialization

// Before
import StoreKit

// After
import AppCoinsSDK

In addition to swapping the import, call AppcSDK.initialize() and AppcSDK.handle(redirectURL:) at every app entry point.

SceneDelegate.swift:

import AppCoinsSDK

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
AppcSDK.initialize()
initialize() // your app setup
if AppcSDK.handle(redirectURL: connectionOptions.urlContexts.first?.url) { return }
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
AppcSDK.initialize()
if AppcSDK.handle(redirectURL: URLContexts.first?.url) { return }
initialize()
}

AppDelegate.swift:

import AppCoinsSDK

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AppcSDK.initialize()
initialize()
if let url = launchOptions?[.url] as? URL {
if AppcSDK.handle(redirectURL: url) { return true }
}
return true
}

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
AppcSDK.initialize()
if AppcSDK.handle(redirectURL: url) { return true }
initialize()
return true
}

2. Check Availability (Single-Build Only)

If you ship one binary that supports both Apple billing and AppCoins billing, guard all SDK calls with:

if await AppcSDK.isAvailable() {
// AppCoins billing path
} else {
// StoreKit 2 / Apple billing path
}

If you use separate builds, one per storefront, skip this guard and always use AppCoins SDK calls in the Aptoide build.

3. transaction.id is String, Not UInt64

StoreKit 2 uses UInt64 for transaction.id. The AppCoins SDK uses String. This is the only type-level API difference.

If you store transaction IDs or compare them against a StoreKit 2 value, cast explicitly:

let appcId: String = transaction.id          // AppCoins SDK
let skId: String = String(storeKitTxn.id) // cast from StoreKit 2 UInt64

What Stays the Same

StoreKit 2AppCoins SDKCompatible?
Product.products(for:)Product.products(for:)Same
product.purchase(options:)product.purchase(options:)Same
Product.PurchaseResultProduct.PurchaseResultSame
VerificationResultVerificationResultSame
Transaction.updatesNot availableAppCoins has no equivalent — use product.purchase() return + Transaction.unfinished
Transaction.unfinishedTransaction.unfinishedSame
Transaction.allTransaction.allSame
transaction.finish()transaction.finish()Same
transaction.productIDtransaction.productIDSame
transaction.purchaseDatetransaction.purchaseDateSame
transaction.appAccountTokentransaction.appAccountTokenSame
Product.PurchaseOption.appAccountTokenProduct.PurchaseOption.appAccountTokenSame
transaction.id (UInt64)transaction.id (String)Type differs

Single-Build Migration

Use this approach when you want one app binary that handles both Apple and Aptoide billing.

Before — StoreKit 2 only:

import StoreKit

class StoreManager {
func loadProducts() async {
do {
let products = try await Product.products(for: ["gas", "turbo"])
// display products
} catch {
print("Failed to load products: \(error)")
}
}

func purchase(_ product: Product) async {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
await transaction.finish()
}
case .pending, .userCancelled:
break
@unknown default:
break
}
} catch {
print("Purchase error: \(error)")
}
}
}

After — AppCoins SDK with single-build guard:

import AppCoinsSDK
import StoreKit

class StoreManager {
func loadProducts() async {
if await AppcSDK.isAvailable() {
let products = try? await AppCoinsSDK.Product.products(for: ["gas", "turbo"])
// display products
} else {
let products = try? await StoreKit.Product.products(for: ["gas", "turbo"])
// display products
}
}

func purchase(sku: String) async {
if await AppcSDK.isAvailable() {
guard let product = try? await AppCoinsSDK.Product.products(for: [sku]).first else { return }
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
await transaction.finish()
}
case .pending, .userCancelled:
break
}
} catch let error as AppCoinsSDKError {
print("Purchase error: \(error)")
}
} else {
guard let product = try? await StoreKit.Product.products(for: [sku]).first else { return }
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
await transaction.finish()
}
case .pending, .userCancelled:
break
@unknown default:
break
}
} catch {
print("Purchase error: \(error)")
}
}
}
}
⚠️
Type ambiguity: When both import StoreKit and import AppCoinsSDK are present, Swift will raise ambiguity errors on Product, Transaction, and VerificationResult. Qualify each reference with the module name: AppCoinsSDK.Product, AppCoinsSDK.Transaction, and StoreKit.Product, StoreKit.Transaction as needed.

Separate-Builds Migration

Use this approach when you maintain one build for the Apple App Store and a separate build for Aptoide.

  1. Create the Aptoide build target (or branch) from your existing StoreKit 2 target.

  2. Swap the import:

    // Before
    import StoreKit

    // After
    import AppCoinsSDK
  3. Add SDK initialization to SceneDelegate.swift or AppDelegate.swift as shown in Import and Initialization above.

  4. Remove @unknown default cases from your Product.PurchaseResult switches if you prefer — the AppCoins Product.PurchaseResult enum is sealed and will not add unknown cases. This is optional; keeping the case is harmless.

  5. Update any code that reads transaction.id — change the type annotation from UInt64 to String:

    // Before (StoreKit 2)
    let txId: UInt64 = transaction.id

    // After (AppCoins SDK)
    let txId: String = transaction.id

No other code changes are required. Every other call site compiles as-is with import AppCoinsSDK.