在 StoreKit 1 基础上接入 AppCoins
本指南介绍如何为已使用 StoreKit 1 的应用接入 AppCoins 计费。具体步骤取决于您的集成策略——开始之前请先阅读集成策略一节。
概览
StoreKit 1 以回调和委托为核心:您通过 SKProductsRequest 请求商品,在 SKProductsRequestDelegate 中接收响应,通过 SKPaymentQueue 提交支付,并在 SKPaymentTransactionObserver 中处理每一次交易状态变化。您还必须手动管理观察者的生命周期,并在恰当的时机显式调用 finishTransaction。
AppCoins 仅支持消耗型应用内购买。 在通过 Aptoide 分发的构建中无法销售订阅和非消耗型商品——AppCoins 不支持这些类型,而替代应用市场构建中也无法使用 StoreKit 计费。这些商品类型在 App Store 构建中仍可照常使用。
AppCoins SDK 会根据 iOS 版本以及应用的分发方式启用——参见运行时检测的原理。
在 AppCoins 计费路径上,SDK 只使用以下三个概念:
Product—— 通过async throws方法获取并购买商品。Transaction—— 使用AsyncStream监听和查询已完成的交易。VerificationResult<Transaction>—— SDK 会在本地验证每一笔交易,并包装其结果。
在 AppCoins 一侧,无需注册观察者、无需实现委托,也无需与 SKPaymentQueue 打交道。
集成策略
提供两套计费路径有两种方式。请参见集成策略指南以确定哪种适合您的情况。
单一构建: 两套计费路径位于同一个二进制文件中。运行时调用 await AppcSDK.isAvailable()——若为 true 则走 AppCoins 路径;若为 false 则回落到您现有的 StoreKit 1 代码。
独立构建: main 分支保持原样(完全不改动 StoreKit 1)。在 aptoide 分支上,用 AppCoins 的实现替换计费相关文件。App Store 的二进制文件中永远不含 AppCoins 代码。
1. 配置
添加 AppCoins SDK Swift Package
在 Xcode 中,从以下地址添加 Swift Package:
https://github.com/Catappult/appcoins-sdk-ios.git
当系统提示选择版本规则时,请选择从最新主版本(例如 5.0.0)开始的 Up to Next Major Version。这样您可以自动获得补丁和次版本更新,同时避免未来主版本带来的破坏性变更。
Xcode 配置
需要完成三项配置:
-
Keychain Sharing
- 在导航器中选择您的工程,并在 TARGETS 下选择您的 target。
- 进入 Signing & Capabilities 标签页,点击 + 添加能力。
- 搜索 Keychain Sharing 并启用。
- 将 Keychain Groups 中自动填入的值替换为
com.aptoide.appcoins-wallet。
-
URL Scheme
- 进入目标的 Info 标签页。
- 在 URL Types 下点击 +,将 URL Scheme 设置为
$(PRODUCT_BUNDLE_IDENTIFIER).iap,角色设置为 Editor。
-
MKSellsDigitalGoods
- 在 Info 标签页中滚动到 Custom iOS Target Properties 并点击 +。
- 添加键
MKSellsDigitalGoods,并将其值设置为YES(Boolean)。
2. 初始化 SDK
请在应用的每个入口点初始化 SDK。在 SceneDelegate.swift 或 AppDelegate.swift 中添加以下调用。
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
}
3. 单一构建:同一二进制中包含两套计费路径
使用 AppcSDK.isAvailable() 在运行时切换计费路径。您现有的 StoreKit 1 代码无需改动——该判断只是在 Aptoide 安装场景下绕过它。
func purchase(sku: String) async {
if await AppcSDK.isAvailable() {
// AppCoins billing path (Aptoide installs)
guard let product = try? await Product.products(for: [sku]).first else { return }
let result = try? await product.purchase()
// ... handle AppCoins result
} else {
// StoreKit 1 path (App Store installs)
let payment = SKPayment(product: skProduct)
SKPaymentQueue.default().add(payment)
// ... your existing SK1 observer handles the result
}
}
4. 获取商品
以下各节展示了 StoreKit 1 各项操作在 AppCoins SDK 中的等效实现。
| StoreKit 1 | AppCoins SDK |
|---|---|
SKProductsRequest + SKProductsRequestDelegate | try await Product.products(for:) |
StoreKit 1:
var products: [SKProduct] = []
func requestProducts() {
let identifiers: Set<String> = ["gas", "turbo"]
let request = SKProductsRequest(productIdentifiers: identifiers)
request.delegate = self
request.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
products = response.products
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Product request failed: \(error)")
}
AppCoins SDK 等效实现:
import AppCoinsSDK
var products: [Product] = []
func requestProducts() async {
do {
products = try await Product.products(for: ["gas", "turbo"])
} catch {
print("Product request failed: \(error)")
}
}
5. 购买商品
| StoreKit 1 | AppCoins SDK |
|---|---|
SKPaymentQueue.default().add(SKPayment(product:)) | try await product.purchase(options:) |
StoreKit 1:
func buy(_ product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
// Then handle the result in paymentQueue(_:updatedTransactions:) — see Section 6
AppCoins SDK 等效实现:
func buy(_ product: Product) async {
do {
let result = try await product.purchase()
switch result {
case .success(let verificationResult):
switch verificationResult {
case .verified(let transaction):
// Grant the item to the user
giveItem(for: transaction.productID)
await transaction.finish()
case .unverified(let transaction, let error):
// Decide based on your business logic
print("Unverified: \(error)")
}
case .pending:
break // Transaction is awaiting approval
case .userCancelled:
break
}
} catch let error as AppCoinsSDKError {
print("Purchase failed: \(error)")
}
}
6. 交易观察者
StoreKit 1 需要注册 SKPaymentTransactionObserver 才能接收购买结果。AppCoins SDK 没有观察者——结果直接由 product.purchase() 返回,无需注册任何内容。
StoreKit 1:
// AppDelegate / SceneDelegate
SKPaymentQueue.default().add(self)
extension YourClass: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
if let error = transaction.error { print(error) }
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred, .purchasing:
break
@unknown default:
break
}
}
}
}
AppCoins SDK 等效实现:
无需观察者。请在调用 product.purchase() 的位置处理结果:
import AppCoinsSDK
func buy(_ product: Product) async {
do {
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
}
} catch let error as AppCoinsSDKError {
print("Purchase failed: \(error)")
}
}
对于未完成的购买(崩溃、强制退出)的恢复,由启动时的 Transaction.unfinished 处理——参见第 7 节。
7. 启动时的未完成交易
StoreKit 1 会通过观察者自动上报未完成的交易。在 AppCoins SDK 中,请在每次启动时显式查询 Transaction.unfinished。
StoreKit 1:
// Transactions were replayed automatically to paymentQueue(_:updatedTransactions:) on every launch
SKPaymentQueue.default().add(self)
AppCoins SDK 等效实现:
func processUnfinishedTransactions() async {
for await verificationResult in Transaction.unfinished {
if case .verified(let transaction) = verificationResult {
giveItem(for: transaction.productID)
await transaction.finish()
}
}
}
请在 AppcSDK.initialize() 之后,于应用启动流程中调用该方法。
8. 恢复购买
StoreKit 1:
SKPaymentQueue.default().restoreCompletedTransactions()
// Results delivered to paymentQueue(_:updatedTransactions:) with state .restored
AppCoins SDK 等效实现:
func restorePurchases() async {
for await verificationResult in Transaction.all {
if case .verified(let transaction) = verificationResult {
giveItem(for: transaction.productID)
await transaction.finish()
}
}
}
Transaction.all 包含该用户曾经记录的所有交易,其中也包括已完成的交易。对每一笔都调用 giveItem() 会重复发放已被消耗的商品。如果您确实需要恢复流程,请在再次发放之前于服务端确认该商品尚未发放——或者在仅包含消耗型商品的应用中完全省略恢复功能。9. 完成交易
| StoreKit 1 | AppCoins SDK |
|---|---|
SKPaymentQueue.default().finishTransaction(transaction) | await transaction.finish() |
AppCoins SDK 中的 transaction.finish() 不会抛出异常,无需用 do/catch 包裹。
10. 主要差异
| StoreKit 1 | AppCoins SDK |
|---|---|
SKProduct.productIdentifier | product.id |
SKProduct.localizedTitle | product.displayName |
SKProduct.price(NSDecimalNumber) | product.price(Decimal) |
SKProduct.priceLocale 加格式化器 | product.displayPrice(已格式化的 String) |
SKPayment / SKPaymentTransaction | Transaction |
paymentQueue(_:updatedTransactions:) | product.purchase() 的返回值,加上启动时的 Transaction.unfinished |
SKPaymentQueue.default().finishTransaction | await transaction.finish() |
transaction.transactionIdentifier(String?) | transaction.id(String) |
SKPaymentQueue.default().add(observer) | 无需注册 |
SKPaymentQueue.default().restoreCompletedTransactions() | Transaction.all 异步流 |