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:
-
Keychain Sharing
- Select your project in the Project Navigator (left sidebar).
- Select your target under TARGETS.
- Go to the Signing & Capabilities tab.
- Click the + button to add a new capability.
- Search for Keychain Sharing and select it.
- In the Keychain Groups field, replace the default value with exactly
com.aptoide.appcoins-wallet.
-
URL Scheme
- Select your target under TARGETS.
- Navigate to the Info tab.
- Expand the URL Types section and click +.
- Set URL Schemes to
$(PRODUCT_BUNDLE_IDENTIFIER).iapand Role to Editor.
-
MKSellsDigitalGoods
- In the Info tab, scroll to the Custom iOS Target Properties section and click +.
- Add the key
MKSellsDigitalGoodsand set its value toYES(Boolean).
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 2 | AppCoins SDK | Compatible? |
|---|---|---|
Product.products(for:) | Product.products(for:) | Same |
product.purchase(options:) | product.purchase(options:) | Same |
Product.PurchaseResult | Product.PurchaseResult | Same |
VerificationResult | VerificationResult | Same |
Transaction.updates | Not available | AppCoins has no equivalent — use product.purchase() return + Transaction.unfinished |
Transaction.unfinished | Transaction.unfinished | Same |
Transaction.all | Transaction.all | Same |
transaction.finish() | transaction.finish() | Same |
transaction.productID | transaction.productID | Same |
transaction.purchaseDate | transaction.purchaseDate | Same |
transaction.appAccountToken | transaction.appAccountToken | Same |
Product.PurchaseOption.appAccountToken | Product.PurchaseOption.appAccountToken | Same |
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)")
}
}
}
}
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.
-
Create the Aptoide build target (or branch) from your existing StoreKit 2 target.
-
Swap the import:
// Before
import StoreKit
// After
import AppCoinsSDK -
Add SDK initialization to
SceneDelegate.swiftorAppDelegate.swiftas shown in Import and Initialization above. -
Remove
@unknown defaultcases from yourProduct.PurchaseResultswitches if you prefer — the AppCoinsProduct.PurchaseResultenum is sealed and will not add unknown cases. This is optional; keeping the case is harmless. -
Update any code that reads
transaction.id— change the type annotation fromUInt64toString:// 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.