Skip to main content

Updating from a previous AppCoins SDK version

This guide covers updating from the previous version of the AppCoins iOS SDK to the current version. The new version aligns with the StoreKit 2 public interface, which required renaming several properties, replacing the Purchase type with Transaction, and removing the PurchaseIntent / Purchase.updates flow entirely.

Overview

The key changes in the current SDK version are:

  • Purchase renamed to TransactionPurchase.all, Purchase.unfinished, and Purchase.latest are now Transaction.all, Transaction.unfinished, and Transaction.latest(for:).
  • PurchaseIntent and Purchase.updates removed — The old Purchase.updates emitted PurchaseIntent objects requiring explicit confirmation. Purchase results are now returned directly from product.purchase() as VerificationResult<Transaction>.
  • Property renames on Productskuid, titledisplayName, priceLabeldisplayPrice.
  • Product.products(domain:for:) simplified — The domain parameter is removed. The SDK reads the bundle identifier automatically.
  • Purchase errors now throw — Replace the .failed(let error) result case with a do/catch around product.purchase().
  • transaction.finish() no longer throws — Remove the try keyword.

Breaking Changes at a Glance

Old APINew API
product.skuproduct.id
product.titleproduct.displayName
product.priceLabelproduct.displayPrice
product.priceValueproduct.displayPrice (formatted) or product.price (Decimal)
Product.products(domain:for:)Product.products(for:) — domain removed
PurchaseResult.failed(let error)throws AppCoinsSDKError — use do/catch
try await purchase.finish()await transaction.finish() — no longer throws
Purchase.unfinished() (async, throws)Transaction.unfinishedAsyncStream, no throw
Purchase.all() (async, throws)Transaction.allAsyncStream
Purchase.latest(sku:)Transaction.latest(for: productID:)
Purchase.updates (stream of PurchaseIntent)Removed — use the return value of product.purchase()
PurchaseIntent.confirm()Removed
PurchaseIntent.reject()Removed

Xcode Configuration

Verify that the following settings are in place on your Xcode target. These are required for the SDK to function:

  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).

1. Updating the Purchase Flow

Before:

let result = await product.purchase()

switch result {
case .success(let verificationResult):
switch verificationResult {
case .verified(let purchase):
try await purchase.finish()
case .unverified(let purchase, let error):
print("Unverified: \(error)")
}
case .pending:
break
case .userCancelled:
break
case .failed(let error):
print("Purchase failed: \(error)")
}

After:

do {
let result = try await product.purchase()

switch result {
case .success(let verificationResult):
switch verificationResult {
case .verified(let transaction):
await transaction.finish()
case .unverified(let transaction, let error):
print("Unverified: \(error)")
}
case .pending:
break
case .userCancelled:
break
}
} catch let error as AppCoinsSDKError {
print("Purchase failed: \(error)")
}

The two differences are:

  1. product.purchase() is now try await product.purchase() — the call can throw.
  2. try await purchase.finish() is now await transaction.finish() — the call no longer throws.

2. Updating Unfinished Transactions

Before:

func processUnfinished() async {
do {
let purchases = try await Purchase.unfinished()
for purchase in purchases {
giveItem(for: purchase.sku)
try await purchase.finish()
}
} catch {
print("Error loading unfinished: \(error)")
}
}

After:

func processUnfinished() async {
if await AppcSDK.isAvailable() {
for await verificationResult in Transaction.unfinished {
if case .verified(let transaction) = verificationResult {
giveItem(for: transaction.productID)
await transaction.finish()
}
}
}
}

Transaction.unfinished is an AsyncStream — iterate it with for await. There is no try and no wrapping array to loop over separately.

⚠️
Call this on every app launch. Users who paid during a previous session will not receive their items until their transactions are finished. Purchases are automatically refunded after 24 hours if not consumed.

3. Updating Transaction.all and Transaction.latest

Before:

let purchases = try await Purchase.all()
let latest = try await Purchase.latest(sku: "gas")

After:

// Iterate all transactions
for await verificationResult in Transaction.all {
if case .verified(let transaction) = verificationResult {
// handle transaction
}
}

// Get the latest transaction for a product
if let verificationResult = await Transaction.latest(for: "gas") {
if case .verified(let transaction) = verificationResult {
// handle transaction
}
}

4. Removing PurchaseIntent Handling

The old SDK exposed Purchase.updates as a stream of PurchaseIntent objects that required explicit confirmation or rejection. Remove all Purchase.updates observation code — there is no replacement stream. Purchase results are returned directly from product.purchase().

Before:

for await intent in Purchase.updates {
if User.isSignedIn {
let result = await intent.confirm()
// handle result
}
}

After: delete this observer entirely. Handle the result where you call product.purchase():

let result = try await product.purchase()
switch result {
case .success(let verificationResult):
if case .verified(let transaction) = verificationResult {
giveItem(for: transaction.productID)
await transaction.finish()
}
case .pending, .userCancelled:
break
}

If you relied on delaying confirmation until a user was authenticated, store the pending product and call product.purchase() once the user has logged in.

5. Error Handling Changes

Before:

switch result {
case .failed(let error):
switch error {
case .networkError:
showNetworkAlert()
case .purchaseNotAllowed:
showNotAllowedAlert()
default:
showGenericAlert()
}
// ...
}

After:

do {
let result = try await product.purchase()
// handle result
} catch let error as AppCoinsSDKError {
switch error {
case .networkError:
showNetworkAlert()
case .purchaseNotAllowed:
showNotAllowedAlert()
case .productUnavailable:
showUnavailableAlert()
case .notEntitled:
showEntitlementAlert()
case .systemError:
showSystemErrorAlert()
case .unknown:
showGenericAlert()
}
}

All AppCoinsSDKError cases remain the same. Only the mechanism for receiving them changed from a result case to a thrown error.