iOS(swift)におけるアプリ内課金(IAP)まとめ

  • 55
    いいね
  • 0
    コメント

以前調べながらまとめたやつです。いろんな記事や公式ドキュメントを参考にさせていただいたはずですが、参考URLとかメモしていなかったようで申し訳有りません・・・。


IAP(In-App Purchase: アプリ内課金)

IAPを使用するとStore Kitフレームワークを使用してアプリケーション内にストアを組み込むことができる。IAPは、追加の機能やコンテンツの支払いの回収に使用する。

IAPで販売可能/不可能なもの

販売可能なもの

  • 仮想通貨などのデジタルコンテンツ
  • ゲームステージ解放などのアプリケーションの機能
  • データへのアクセスなどの1回または継続的なサービス

販売不可能なもの

実物の商品やサービス、不適切なコンテンツはNG

課金タイプ

消耗型(Consumable)

何度でも課金可能で、1度使ったら無効になったり減ったりするようなもの。アプリ内で使う仮想通貨など。仮想通貨に有効期限を与えてはいけない。また、購読でもないのにコンテンツの表示期間を設けるのはNG。

アプリ内課金で買ったものはそのアプリ内で使わなくてはならない。Andoridに引き継いだりするのも規約的にはまずい。GooglePlayでもアプリ内仮想通貨をアプリ内以外で使ってはならないという規約があり、両プラットフォームで今後厳しくなってくる可能性が高い。

非消耗型(Non-consumable)プロダクト

1度だけ課金。アプリ内広告の非表示など、使っても無効になったり減ったりしないサービス。

自動更新購読(Auto-renewable subscriptions)

雑誌の購読や期間限定のメンバー会員などの動的なコンテンツ。購読はユーザが解除しない限り自動的に更新される。

非更新購読(Non-renewable subscriptions)

一定期間のナビゲーションアプリ内でのガイダンス機能など、期間を限定した項目を販売。時間ベースで静的コンテンツにアクセスできる製品に使用。

無料購読(Free subscriptions)

Newsstandに無料購読のコンテンツを置くための手段。サインアップしたユーザは、Apple IDに関連付けられたどのデバイスからでも購読できる。期限切れになることはない。また、購読にはNewsstand対応アプリケーションが必要。

実装手順

次のように実装をしていく。

  1. プロダクト情報の取得
  2. 支払いの要求
  3. プロダクトの配信
  4. 購読に必要なアプリケーションロジックの追加
  5. ユーザーが購入したプロダクトの復元
  6. アプリケーションとプロダクトを審査用に登録

1. プロダクト情報の取得

購入プロセスの最初の段階として、アプリケーションはApp Storeからプロダクトに関する情報を取得し、ユーザーに対してストアUIを表示した後、ユーザーがプロダクトを選択する。

プロダクトIDのリストの取得

アプリケーションで販売するすべてのプロダクトには一意のプロダクトIDがある。広告の削除や機能の有効化など固定された目的のタイプのプロダクトIDはアプリケーション内に埋め込んでも良いが、アプリケーションの更新をせずにリストを更新するためにはサーバーからリストをフェッチするようにする。

プロダクト情報の取得

ストアUIを表示する前に、App Storeからユーザーが実際に購入できるプロダクトの一覧を取得する。これにより、無効なプロダクトIDおよび有効なプロダクトについての情報が取得できる。KVOを使った実装は次のようになる。

import UIKit
import StoreKit

class ProductManager: NSObject {
    static var sharedInstance = ProductManager()

    var request: SKProductsRequest!
    dynamic var products: [SKProduct]!

    // 課金商品の一覧をバックエンドのDBから取得
    func fetchProducts() {

    }

    // Product情報をApp Storeから取得
    func validateProductIdentifiers(productIdentifiers:[String]) {
        self.request = SKProductsRequest(productIdentifiers: Set<String>(productIdentifiers))
        request.delegate = self
        request.start()
    }
}

extension ProductManager: SKProductsRequestDelegate {
    func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
        self.products = response.products
        for id in response.invalidProductIdentifiers {
            // Handle any invalid product identifiers.
            print(id)
        }
    }
}

アプリケーションのストアUIの表示

アプリケーションのストアUIをデザインするときには、次のガイドラインを考慮する。

  • ユーザーが支払いを行える場合にのみストアを表示する。
  • アプリケーションのフローの中で自然にプロダクトを表示する
  • 楽しく簡単に体験ができるようにプロダクトを構成する
  • ユーザーにとってのプロダクトの価値を伝える
  • App Storeによって返されるロケールと通貨を使用して価格を明示する

次のようなヘルパーを使用すると良い。

import UIKit
import StoreKit

class IAPHelper: NSObject {

    // ユーザーが課金可能かどうか
    class func canMakePayments() -> Bool {
        return SKPaymentQueue.canMakePayments()
    }

    // 適切にフォーマットされた価格の返却
    class func formattedPrice(product: SKProduct) -> String? {
        let formatter = NSNumberFormatter()
        formatter.formatterBehavior = .Behavior10_4
        formatter.numberStyle = .CurrencyStyle
        formatter.locale = product.priceLocale
        return formatter.stringFromNumber(product.price)
    }

}

2. 支払いの要求

支払いの要求は、購入プロセスの第2段階となる。

異常アクティビティの検知

ユーザーがApp Storeアカウントに加えてアプリケーションのアカウントを持っている場合、支払い要求の際にこの追加情報を提供することで異常アクティビティの検知能力が向上する。この情報を提供するためには、支払いオブジェクトのapplicationUsernameプロパティにアカウント名の一方向ハッシュを入力する。

これを実現するためには、Bridging-Headerで#import <CommonCrypto/CommonCrypto.h>した上で、次のようにStringのextensionを作ると良い。

import UIKit

extension String {
    func sha256String() -> String {
        let cstr = self.cStringUsingEncoding(NSUTF8StringEncoding)
        let data = NSData(bytes: cstr!, length: self.length)

        var digest = [UInt8](count: Int(CC_SHA256_DIGEST_LENGTH), repeatedValue: 0)

        CC_SHA256(data.bytes, CC_LONG(data.length), &digest)

        let output = NSMutableString(capacity: 64)
        for i in 0..<32 {
            output.appendFormat("%02x", digest[i])
        }

        return output as String
    }
}

支払い要求の送信

ユーザーがプロダクトを購入することを選択したあと、支払い要求を作成し、必要に応じて数量を設定する。トランザクションキューに支払い要求を追加すると、App Storeに送信される。
キューに支払いオブジェクトを複数回追加すると、オブジェクトは複数回送信され、ユーザーは複数回課金されアプリケーションのプロダクトは複数回配信される。

アプリケーションが支払い要求を送信するたびに、対応するトランザクションが発生し、処理が必要になる。

class ProductManager: NSObject {
    // ...

    // 支払い要求をApp Storeに送信
    func buyProduct(product: SKProduct) {
        let payment = SKMutablePayment(product: product)
        payment.applicationUsername = UserManager.sharedInstance.currentUser.token.sha256String()
        SKPaymentQueue.defaultQueue().addPayment(payment)
    }
}

3. プロダクトの配信

購入プロセスの最後の段階では、App Storeによる支払い要求の処理後、アプリケーションは今後の起動に備えて情報を保存し、購入したコンテンツをダウンロードして、トランザクションに終了のマークをつける。

App Storeがトランザクションを処理するのを待機する

支払い要求など、App Storeによる処理が必要な作業はトランザクションキューに追加する。例えばトランザクションの状態が支払い要求に成功して変化した場合、トランザクションキューのオブザーバーが呼び出される。支払い要求のほかに、Appleによってホストされたコンテンツのダウンロードと購読の更新の検出にもトランザクションキューが使用される。

トランザクションキューのオブザーバーはアプリケーションの起動時に登録する。オブザーバーはいつでもトランザクションを処理できる必要がある。例えば、トンネルに入る直前にユーザーがアプリケーションで何かを購入する場合について考慮する。ネットワーク接続がないため、アプリケーションは購入されたコンテンツを配信できない。アプリケーションが次回起動されたときに購入された項目を配信する。

同様に、アプリケーションがトランザクションに終了のマークをつけられなかった場合、トランザクションが終了したと適切にマークされるまで、Store Kitはアプリケーションが起動するたびに毎回オブザーバーを呼び出す。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // ...
    SKPaymentQueue.defaultQueue().addTransactionObserver(ProductManager.sharedInstance)
    // ...
}

トランザクションの状態は、アプリケーションで実行する必要があるアクションがどれであるのかを通知する。キューに入っているトランザクションの状態が変わる順序は不定で、アプリケーションはいつでもアクティブなトランザクションを処理できる必要がある。

状態 アプリケーションで実行するアクション
SKPaymentTransactionStatePurchasing 進捗している状態を反映するようにUIを更新し、再び呼び出されるのを待機する
SKPaymentTransactionStateDeferred 遅延している状態を反映するようにUIを更新し、再び呼び出されるのを待機する
SKPaymentTransactionStateFailed errorプロパティの値を使用して、ユーザーにメッセージを表示する。エラーを表す定数についてはSKErrorDomainについて調べる。
SKPaymentTransationStatePurchased 購入した機能を提供する
SKPaymentTransactionStateRestored 以前に購入した機能を復元する

購入の持続

プロダクトの提供を開始した後、アプリケーションは購入の持続的な記録を作成する必要がある。アプリケーションは起動時にその持続的な記録を使用してプロダクトの提供を継続する。また、購入の復元にも記録を使用する。

  • 非消耗型プロダクトと自動更新購読では、アプリケーションのレシートを持続的な記録として使用する。
  • 非更新購読ではiCloudや独自のサーバーを使用して持続的な記録を管理する。
  • 消耗型プロダクトの場合は、持続的な記録を保持する必要はない

Appレシートを使用した持続

Appレシートには、ユーザの購入記録とAppleによって暗号化された署名が含まれる。
消耗型プロダクトと非更新購読の情報は、支払いが行われるとシートに追加され、トランザクションを終了するまでレシート上に残る。トランザクションの終了後、この情報はレシートが次に更新されるとき、たとえばユーザが次に購入を行ったときに削除される。これ以外の種類のプロダクト購入の情報は、支払いが行われるとレシートに追加され、レシートに残り続ける。

独自サーバーからのコンテンツのダウンロード

アプリケーションからサーバーにレシートを送信し、コンテンツを要求する。サーバーはレシートを適切に検証し、プロダクトが購入済みであることを確立する。レシートが有効である場合は、サーバーはアプリケーションに対してコンテンツで応答する。

トランザクションの終了

トランザクションが終了すると、購入で必要なすべての処理が完了したことがStore Kitに通知される。終了していないトランザクションは終了するまではキューに残されたままになる。アプリケーションが起動されるたびにアプリケーションによって未完のトランザクションを終了できるように、トランザクションキューのオブザーバが呼び出される。アプリケーションは、処理結果(成功か否か)にかかわらず、トランザクションをすべて終了させる必要がある。トランザクションを終了する前に、「購入の持続」「関連コンテンツのダウンロード」「ユーザーがプロダクトにアクセスできるためのアプリケーションUIの更新」の全てを完了させる必要がある。

トランザクションを終了するには、支払いキューでfinishTransaction:メソッドを呼び出す。

トランザクションの終了後には、終了したトランザクションでアクションを実行したり、プロダクトを配信したりする動作を一切行わないこと。

これらに従い、トランザクションキューのオブザーバは次のようなメソッドになる。

extension ProductManager: SKProductsRequestDelegate, SKPaymentTransactionObserver {
    // ...

    func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .Purchasing:
                self.showTransactionAsInProgress(transaction, deferred: false)
            case .Deferred:
                self.showTransactionAsInProgress(transaction, deferred: true)
            case .Failed:
                self.failedTransaction(transaction)
            case .Purchased:
                self.completeTransaction(transaction)
            case .Restored:
                self.restoreTransaction(transaction)
            }
        }
    }

    func showTransactionAsInProgress(transaction: SKPaymentTransaction, deferred: Bool) {
    }

    func failedTransaction(transaction: SKPaymentTransaction) {
    }

    // 購入の成功
    func completeTransaction(transaction: SKPaymentTransaction) {
        guard let product = self.productOfID(transaction.payment.productIdentifier) else { return }
        self.sendReceipt(product) // プロダクトのレシートをサーバーに送信
    }

    func restoreTransaction(transaction: SKPaymentTransaction) {
    }

    func paymentQueue(queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
        // 対応する項目をアプリケーションのUIから削除
    }

    // トランザクションの復元に成功
    func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
    }

    // トランザクションの復元に失敗
    func paymentQueue(queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: NSError) {
    }
}

推奨されるテスト手順

テスト用アカウントによるApp Storeへのサインイン

テストの際には、設定からサインアウトして、アプリケーションで購入を試みる。この際、[Environment: Sandbox]というテキストが表示されることを確認する。

支払い要求のテスト

既にテスト済みの有効なプロダクトIDを使用してSKPaymentのインスタンスを作成する。トランザクションキューに支払い要求を追加し、paymentQueue:updatedTransactions:メソッドが呼び出されることを確認する。

テスト中は、コンテンツを提供せずに直ちにトランザクションを終了しても構わない。

オブザーバーコードの検証

トランザクションオブザーバーのSKPaymentTransactionObserverプロトコルの実装を確認する。アプリケーションのストアUIが表示されておらず、また最近購入を行っていない場合であっても、オブザーバーがトランザクションを処理できることを確認する。

コードでSKPaymentQueueaddTransactionObserver:メソッドが呼び出されている場所を探す。アプリケーションの起動時にこのメソッドが呼び出されていることを検証する。

成功したトランザクションのテスト

テスト用ユーザーアカウントでApp Storeにサインインし、アプリケーションで購入を行う。トランザクションキューのオブザーバーのpaymentQueue:updatedTransactions:メソッドの実装にブレークポイントを設定し、トランザクションを調査し、その状態がSKPaymentTransactionStatePurchasedであることを確認する。

中断したトランザクションのテスト

トランザクションキューのオブザーバーのpaymentQueue:updatedTransactions:メソッドにブレークポイントを設定し、プロダクトを配信するかどうかを制御できるようにする。テスト環境で通常通り購入をし、ブレークポイントでトランザクションを一時的に無視する。例えばLLDBのthread returnコマンドを使用してメソッドから直ちに復帰する。

終了してアプリケーションを再起動する。起動後すぐにStore KitがpaymentQueue:updatedTransactions:メソッドを再び呼び出す。このときアプリケーションで正常に応答する。アプリケーションでプロダクトが正しく配信され、トランザクションが完了したことを確認する。

トランザクションが終了したことの確認

アプリケーションでfinishTransaction:メソッドが呼び出されている場所を特定する。このメソッドが呼び出される前にトランザクションに関係するすべての作業が完了していることを確認し、成功か否かにかかわらずこのメソッドが呼び出されることを確認する。

4. 購読の取り扱い

購読を使用するアプリケーションについては、追加の動作と考慮事項がある。購読には時間的要素が組み込まれているため、アプリケーションは購読が現在有効であるかどうかと、サブスクリプションが有効だったのが過去のどの期間であったのかを判断する適切なロジックが必要。アプリケーションは新機能購読と購読の更新にも対応し、また期限切れの購読も適切に処理する必要がある。

期限切れと更新

更新プロセスは、期限切れの10日前から開始される「事前」チェックから始まる。この10日の間に、例えば、顧客に有効な支払い方法があるかどうか、ユーザーがこの購読を購入してからプロダクトの価格が上昇していないかどうか、またはプロダクトが利用できなくなっていないかなど、購読の自動更新が遅れたり更新できなくなる可能性のある問題がApp Storeによってチェックされる。問題がある場合はApp Storeによってユーザーに通知されるため、購読の更新が必要になる前に問題を戒家することができ、購読が中断されないようにする。

購読が期限切れになる前の24時間の期間中に、App Storeは購読の自動更新を開始する。App Storeは一定期間、数回にわたって購読の自動更新を試みるが、失敗する回数が多いと最終的に更新を停止する。

購読の更新に成功すると、Store Kitはトランザクションキューに更新用のトランザクションを追加する。アプリケーションは起動時にトランザクションキューをチェックして、他のトランザクションと同じ方法で更新の処理を行う。

キャンセル

購読の購入時には全額が支払われるが、(操作ミスによる購入時などは)Appleカスタマーサービスに連絡することによって払い戻されることがある。ただし、購読期間の途中で気が変わって、残りの期間について支払わないということはできない。

購入がキャンセルされたかどうかチェックするにはレシートの「Cancellation Date」フィールドを確認する。キャンセルされたレシートは、購入が行われなかったものとして扱われる。

ユーザーによる購読管理

アプリケーションに購読管理用のUIを実装せず、次のURLを開く形にしても構わない。

https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/manageSubscriptions

このURLを開くとiTunesまたはiTunes Storeが起動し、「Manage Subscription」ページが現れる。

テスト環境

自動更新購読の本番環境とテスト環境の間には動作の点で多少の相違がある。

更新が発生する頻度が速くなり、自動更新購読が1日で最大6回更新される。これにより、購読の更新、購読の執行、購読期間に感覚がある購読履歴がアプリケーションでどのように処理されるかをテストできる。

5. 購入したプロダクトの復元

ユーザーは購入済みのコンテンツに引き続きアクセスするためにトランザクションを復元する。ほとんどの場合、アプリケーションで必要な処理は、レシートの更新とレシートに含まれるプロダクトの配信のみ。

レシートを更新すると、App Storeにレシートの最新コピーが要求される。レシートを更新しても新しいトランザクションは作成されない。

Appレシートの更新

レシート更新要求を作成し、デリゲートを設定して、要求を開始する。要求では、期限切れのレシートなど、テスト中の様々な状態のレシートの取得のためのオプションのプロパティがサポートされる。SKReceiptRefreshRequestinitWithReceiptProperties:メソッドの値を参照すること。

6. アプリケーション審査の準備

プロダクトを審査用に登録する

アプリケーションを始めて審査のために登録する場合は、同時に審査を受けるためにアプリケーション内プロダクトも登録する必要がある。最初の登録後は、審査のためのアプリケーションおよびプロダクトに対する更新の登録をそれぞれ独自に行える。

テスト環境のレシート

ユーザーのサーバーでレシートを検証する場合、サーバーでは本番用として署名されたアプリケーションを処理して、そのレシートをAppleのテスト用環境から受け取ることができる必要がある。本番のApp Storeでレシートの検証をし、検証が「Sandbox receipt used in production」エラーで失敗したら、本番用の代わりにテスト用の環境に対する検証を行うと良い。

実装におけるチェックリスト

最後に、以下が正しく実装されているか確認する。

  • iTunes Connectでプロダクトを作成し、設定する
  • アプリケーションバンドルまたは独自のサーバーからプロダクトIDのリストを取得する。SKProductsRequestのインスタンスを使用してそのリストをApp Storeに送信する
  • App Storeから返されたSKProductのインスタンスを使用して、App Store向けのユーザーインターフェースを実装する。
  • SKPaymentQueueaddPayment:メソッドを使用してSKPaymentのインスタンスをトランザクションキューに追加することで支払いを要求する。
  • paymentQueue:updateTransactions:メソッドからトランザクションキューのオブザーバーを実装する。適切な段階でSKPaymentTransactionObserverプロトコルの他のメソッドを実装する。
  • アプリケーションの今後の起動に備えて購入記録を持続し、関連するコンテンツをダウンロードした後、SKPaymentQueuefinishTransaction:メソッドを呼び出す。
  • 非消耗型、自動更新購読、非更新購読を販売する場合、復元プロセスを開始するためのUIを提供する。SKRecieptRefreshRequestクラスを使用してAppレシートを更新するか、SKPaymentQueueクラスのrestoreCompletedTransactionsメソッドを使用して完了したトランザクションを復元することで過去の購入情報を取得する。ユーザーがコンテンツを再ダウンロードするようにする。Appleによってホストされたコンテンツを使用している場合、完了したトランザクションを復元し、トランザクションのdownloadsプロパティを使用してSKDownloadのインスタンスを取得する。
  • 自動更新、非更新購読を販売する場合、最新の発行済コンテンツを配信することで新しく購入した購読を処理する。新規コンテンツが発行された場合、ユーザーが参照できるようにする。購読の有効期限が切れた場合、ユーザーが更新するようにする。アプリケーションで自動更新購読を販売する場合、App Storeがこのプロセスを処理する。購読が無効になった時点で新規コンテンツの提供を停止する。購読を再度有効にして購入するオプションがユーザーに表示されるよう、インターフェースを更新する。コンテンツが発行された場合に追跡するためのシステムを実装する。購入を復元済みの場合はこのシステムを使用して、購読がアクティブだった期間に基づき、ユーザーが支払い済みのコンテンツにアクセスできるようにする。