StoreKit 1 と併用して AppCoins を追加する
このガイドでは、すでに StoreKit 1 を使用しているアプリに AppCoins 課金を追加する方法を説明します。手順は連携戦略によって異なります。開始する前に 連携戦略 のセクションをお読みください。
概要
StoreKit 1 はコールバックとデリゲートを中心に構成されています。SKProductsRequest で商品をリクエストし、SKProductsRequestDelegate でレスポンスを受け取り、SKPaymentQueue で支払いをキューに入れ、SKPaymentTransactionObserver ですべてのトランザクション状態の変化を処理します。さらに、オブザーバーのライフサイクルを手動で管理し、適切なタイミングで明示的に finishTransaction を呼び出す必要があります。
AppCoins は消費型のアプリ内購入のみをサポートします。 Aptoide 経由で配信されるビルドでは、サブスクリプションおよび非消費型商品を販売できません。AppCoins がこれらをサポートしておらず、代替マーケットプレイス向けビルドでは StoreKit 課金も利用できないためです。これらの商品タイプは、App Store 向けビルドでは従来どおり動作します。
AppCoins SDK は、iOS のバージョンとアプリの配信方法に基づいて有効になります。実行時判定の仕組み を参照してください。
AppCoins 課金の経路では、AppCoins SDK は次の 3 つの概念のみを使用します。
Product—async throwsのメソッドで商品を取得し、購入します。Transaction—AsyncStreamを使用して完了したトランザクションを監視・照会します。VerificationResult<Transaction>— SDK がすべてのトランザクションをローカルで検証し、その結果をラップします。
AppCoins 側では、登録すべきオブザーバーも、準拠すべきデリゲートも、操作すべき SKPaymentQueue もありません。
連携戦略
両方の課金経路を提供する方法は 2 つあります。どちらが自身の構成に適しているかを判断するには、連携戦略ガイド を参照してください。
単一ビルド: 両方の課金経路が同じバイナリに含まれます。実行時に 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 の設定
次の 3 つの設定手順が必要です。
-
Keychain Sharing
- ナビゲータでプロジェクトを選択し、TARGETS でターゲットを選択します。
- Signing & Capabilities タブを開き、+ をクリックして機能を追加します。
- Keychain Sharing を検索して有効にします。
- Keychain Groups に自動入力された値を
com.aptoide.appcoins-walletに置き換えます。
-
URL スキーム
- ターゲットの Info タブに移動します。
- URL Types で + をクリックし、URL スキームを
$(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. 単一ビルド: 1 つのバイナリに両方の課金経路を含める
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 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() は throw しません。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 の非同期ストリーム |