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:
Purchaserenamed toTransaction—Purchase.all,Purchase.unfinished, andPurchase.latestare nowTransaction.all,Transaction.unfinished, andTransaction.latest(for:).PurchaseIntentandPurchase.updatesremoved — The oldPurchase.updatesemittedPurchaseIntentobjects requiring explicit confirmation. Purchase results are now returned directly fromproduct.purchase()asVerificationResult<Transaction>.- Property renames on
Product—sku→id,title→displayName,priceLabel→displayPrice. Product.products(domain:for:)simplified — Thedomainparameter is removed. The SDK reads the bundle identifier automatically.- Purchase errors now throw — Replace the
.failed(let error)result case with ado/catcharoundproduct.purchase(). transaction.finish()no longer throws — Remove thetrykeyword.
Breaking Changes at a Glance
| Old API | New API |
|---|---|
product.sku | product.id |
product.title | product.displayName |
product.priceLabel | product.displayPrice |
product.priceValue | product.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.unfinished — AsyncStream, no throw |
Purchase.all() (async, throws) | Transaction.all — AsyncStream |
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:
-
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).
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:
product.purchase()is nowtry await product.purchase()— the call can throw.try await purchase.finish()is nowawait 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.
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.