メインコンテンツまでスキップ

在 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 配置

需要完成三项配置:

  1. Keychain Sharing

    1. 在导航器中选择您的工程,并在 TARGETS 下选择您的 target。
    2. 进入 Signing & Capabilities 标签页,点击 + 添加能力。
    3. 搜索 Keychain Sharing 并启用。
    4. Keychain Groups 中自动填入的值替换为 com.aptoide.appcoins-wallet
  2. URL Scheme

    1. 进入目标的 Info 标签页。
    2. URL Types 下点击 +,将 URL Scheme 设置为 $(PRODUCT_BUNDLE_IDENTIFIER).iap,角色设置为 Editor
  3. MKSellsDigitalGoods

    1. Info 标签页中滚动到 Custom iOS Target Properties 并点击 +
    2. 添加键 MKSellsDigitalGoods,并将其值设置为 YES(Boolean)。

2. 初始化 SDK

请在应用的每个入口点初始化 SDK。在 SceneDelegate.swiftAppDelegate.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
}
}
ℹ️
AppcSDK.isAvailable() 在符合条件的安装场景下返回 true(参见运行时检测的原理)。在其他所有设备上它返回 false,您的 StoreKit 1 代码将照常运行。

4. 获取商品

以下各节展示了 StoreKit 1 各项操作在 AppCoins SDK 中的等效实现。

StoreKit 1AppCoins SDK
SKProductsRequest + SKProductsRequestDelegatetry 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 1AppCoins 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

⚠️
请在应用每次启动时查询并完成未完成的交易。在此前会话中已付款的用户,只有在交易被完成后才能收到商品。购买若未被消耗,将在 24 小时后自动退款。

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()
}
}
}
⚠️
AppCoins 仅支持消耗型商品。 Transaction.all 包含该用户曾经记录的所有交易,其中也包括已完成的交易。对每一笔都调用 giveItem() 会重复发放已被消耗的商品。如果您确实需要恢复流程,请在再次发放之前于服务端确认该商品尚未发放——或者在仅包含消耗型商品的应用中完全省略恢复功能。

9. 完成交易

StoreKit 1AppCoins SDK
SKPaymentQueue.default().finishTransaction(transaction)await transaction.finish()

AppCoins SDK 中的 transaction.finish() 不会抛出异常,无需用 do/catch 包裹。

10. 主要差异

StoreKit 1AppCoins SDK
SKProduct.productIdentifierproduct.id
SKProduct.localizedTitleproduct.displayName
SKProduct.priceNSDecimalNumberproduct.priceDecimal
SKProduct.priceLocale 加格式化器product.displayPrice(已格式化的 String
SKPayment / SKPaymentTransactionTransaction
paymentQueue(_:updatedTransactions:)product.purchase() 的返回值,加上启动时的 Transaction.unfinished
SKPaymentQueue.default().finishTransactionawait transaction.finish()
transaction.transactionIdentifierString?transaction.idString
SKPaymentQueue.default().add(observer)无需注册
SKPaymentQueue.default().restoreCompletedTransactions()Transaction.all 异步流