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

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 つの概念のみを使用します。

  • Productasync throws のメソッドで商品を取得し、購入します。
  • TransactionAsyncStream を使用して完了したトランザクションを監視・照会します。
  • 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 つの設定手順が必要です。

  1. Keychain Sharing

    1. ナビゲータでプロジェクトを選択し、TARGETS でターゲットを選択します。
    2. Signing & Capabilities タブを開き、+ をクリックして機能を追加します。
    3. Keychain Sharing を検索して有効にします。
    4. Keychain Groups に自動入力された値を com.aptoide.appcoins-wallet に置き換えます。
  2. URL スキーム

    1. ターゲットの Info タブに移動します。
    2. URL Types+ をクリックし、URL スキームを $(PRODUCT_BUNDLE_IDENTIFIER).iap、ロールを Editor に設定します。
  3. MKSellsDigitalGoods

    1. Info タブで Custom iOS Target Properties までスクロールし、+ をクリックします。
    2. キー 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 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() は throw しません。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 の非同期ストリーム