こんにちは。 マネーフォワードクラウド確定申告アプリiOSエンジニアの佐藤です。 本稿ではマネーフォワードクラウド確定申告に実装したiOSアプリ内課金の設計指針について紹介します。
はじめに
皆さん、iOSアプリ内課金という単語はご存知でしょうか。 iOSアプリ内課金とは、ユーザーが利用するアプリ内または App Store から一連の購入処理を踏むことで、iOSアプリに様々な追加コンテンツやサービスを提供できる機能です。
iOSアプリ内課金の種類
主にアプリ内課金には4つのタイプが存在します。Apple 公式サイトではこちらで説明されています。 弊社確定申告アプリにおける課金タイプは、追加コンテンツを解約するまで利用できる「自動更新サブスクリプション」形式を採用しました。 そのため本稿は自動更新サブスクリプションに少しフォーカスを当てた紹介となります。
種類 | 詳細 | 例 |
---|---|---|
消耗型 | 一度使うことでなくなる。再度購入可能。 | ゲーム内で使用するライフや宝石など |
非消耗型 | 一度の購入で無制限に使用可能。 | 広告の非表示など |
自動更新サブスクリプション | ユーザーが解約するまで定期的に課金する。 | 月額サービスをはじめとした定期的にアップデートされるコンテンツなど |
非更新サブスクリプション | 一定期間無制限に利用可能。ユーザー自身が毎回更新する必要あり。 | ストリーミングコンテンツをはじめとしたシーズンパスなど |
実装に必要なフレームワーク
アプリ内課金を実装する際、StoreKitと呼ばれるフレームワークを使用し購入処理を行います。 まずは StoreKit について説明します。
StoreKit とは?
アプリ内課金における App Store とのやり取りをサポートするフレームワークです。 その際に必要となる主なクラス等を一部抜粋して紹介します。
SKProduct
ユーザーに提供する課金アイテムを表すクラスです。
課金アイテムを一意に識別する productIdentifier: String
や商品価格 price: NSDecimalNumber
など予め App Store Connect
で登録された情報を取得できます。
SKPayment
課金アイテムの購入処理リクエストを表すクラスです。
前述の SKProduct
のインスタンスを利用し、ユーザーが購入したい課金アイテムと数量を指定することで生成できます。
SKPaymentQueue
購入処理を行うため App Store と通信するインターフェースを提供するクラスです。
SKPayment
を enqueue
する度、SKPaymentQueue
は購入するためのトランザクション(SKPaymentTransaction
)を作成します。
queue
の内容についてはアプリ起動間で保持される仕組みとなっています。
SKPaymentTransaction
トランザクションの現在の状態を表すクラスです。 課金実装時はトランザクションの状態に応じ、適切な処理を行う必要があります。
ステータス(SKPaymentTransactionState) | トランザクションの状態 |
---|---|
purchasing | App Storeで処理されている |
purchased | 正常に処理された |
failed | 処理が失敗した |
restored | 以前購入した課金アイテムを復元する |
defferd | 購入制限により処理が保留されている |
SKTransactionObserver
トランザクションの状態を監視するためプロトコルです。
queue
がトランザクションを追加または更新等を行うと SKPaymentTransactionObserver
から通知されます。
アプリ内課金のシーケンス図
アプリ内課金における大まかな流れは以下の通りとなります。それぞれ順を追って説明します。
2. トランザクションを監視
アプリ起動時での課金をはじめ、App Store や iOS 標準の設定アプリにおいても課金アイテム購入は行えるためアプリ起動間は常に監視しておく必要があります。よって、アプリ起動直後にトランザクションを監視する Observer を追加するのが最も望ましいとされています。
import UIKit import StoreKit class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // アプリ起動時に paymentQueue に追加 SKPaymentQueue.default().add(StoreManager.shared) return true } func applicationWillTerminate(_ application: UIApplication) { // アプリ終了時に paymentQueue から削除 SKPaymentQueue.default().remove(StoreManager.shared) } } final class StoreManager: NSObject, SKPaymentTransactionObserver { static let shared = StoreManager() override private init() { super.init() } }
5. 課金アイテム情報を取得
課金アイテム作成時に定義した一意の文字列(ProductIdentifier
)を元に App Store に問い合わせることで対象の課金アイテム及び購入可能状態か確認出来ます。
var request: SKProductsRequest! func fetchProducts() { let productIdentifiers = Set(productIdentifiers) request = SKProductsRequest(productIdentifiers: productIdentifiers) request.delegate = self request.start() }
ProductIdentifier
の管理方法は「Bundleに含める方法」と「サーバーで管理する方法」の2パターン存在します。
弊社確定申告アプリでは、課金アイテムは1つのみであり、かつ今後数多くの課金アイテム追加を検討していないことから、クライアント側で完結する Bundle
方式を採用しました。
8. 購入リクエスト
購入可能な課金アイテムは、購入リクエストを App Store へ送ることで購入処理に移行できます。
購入処理開始時は SKPaymentQueue
に enqueue
することで購入するためのトランザクションが生成され、その後検知している Observer
に対象のトランザクションが通知されます。
let payment = SKPayment(product: selectedProduct) // queue に追加することでトランザクションが生成 SKPaymentQueue.default().add(payment)
トランザクションは SKPaymentTransactionObserver
で確認できます。そこでトランザクションの状態( SKPaymentTransaction)に応じ適切な処理を行う必要があります。
final class StoreManager: NSObject, SKPaymentTransactionObserver { // トランザクションの状態が通知 func paymentQueue(_ queue: SKPaymentQueue,updatedTransactions transactions: [SKPaymentTransaction]) { // トランザクションの状態に応じて処理を記述 handleTransactions(transactions) } }
11~12. レシート検証
App Store に自前サーバーからPOSTリクエストを行いレシートデータを問い合わせることで現在の課金状況を把握できます。このお問い合わせ処理とクライアント側から受け取ったレシートデータが有効なものか確認する処理を総称してレシート検証と呼びます。
クライアント側で最新のレシートデータを取得するには、Bundle.main.appStoreReceiptURL
メソッドでアプリのレシートがある場所を特定できます。取得後はサーバー側にレシートデータを送信するため Base64
に変換する必要があります。
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { do { let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) let receiptString = receiptData.base64EncodedString(options: []) // レシートデータを送信 } catch {} }
15. トランザクションを終了させる通知を送信
一連の購入処理が完了した場合、finishTransaction(_:)
メソッドを呼び出しトランザクションが終了したことを App Store に通知する必要があります。
トランザクションは終了通知を送らない限りqueue
に残り続けてしまいます。例えば、購入途中にアプリを終了してしまっても次回アプリ起動時に復帰できる効力を持ちつつも、大量のトランザクションが残り続けることで処理時間の低下や思わぬ不具合を誘発する恐れがあります。
そのため、レシート検証完了及び課金アイテムの購入処理で必要となるステップが全て完了した際は忘れずに終了通知を呼ぶ必要があります。
SKPaymentQueue.default().finishTransaction(_:)
復元処理について
課金アイテムをデバイス間で同期および復元を想定した基盤を構築する必要があるため、自動更新サブスクリプションなどの課金タイプにおいては「復元(Restore
)機能」を実装する必要があります。本機能を実装しない場合、審査時にリジェクトされてしまうため注意が必要です。
また復元処理に伴う実装方法については、Restore Completed Transactions
と Refresh Receipt
の2パターン存在します。弊社確定申告アプリの事例も交えて、順を追って説明します。
Restore Completed Transactions
完了したトランザクションを全て復元するスタンダードな方法です。復元処理が完了したらレシートをバリデーションに通した上で finishTransaction(_:)
を再度呼ぶ必要があります。
SKPaymentQueue.default().restoreCompletedTransactions()
Refresh Receipt
App Store から最新のレシートを要求して端末のローカルに保存する方法です。新しくトランザクションを発行する必要はありません。
var request: SKProductsRequest! func requestProductInformation(with productIdentifiers: [String]) { let productIdentifiers = Set(productIdentifiers) request = SKReceiptRefreshRequest(receiptProperties: [SKReceiptPropertyIsExpired: false]) request.delegate = self request.start() }
弊社確定申告アプリでは、復元処理も購入処理同様サーバー側にレシートデータを送信しレシート検証を行っています。そのため Restore Completed Transactions
を採用した場合、バリデーションが成功しない限り finishTransaction(_:)
メソッドは呼ばれることはなく、結果として失敗したトランザクションは半永久的に queue
に残り続けてしまうことが懸念されました。
よって弊社確定申告アプリの復元実装方法としては、上記懸念点を考慮しトランザクションが発生しない Refresh Receipt
を採用しました。
おわりに
本稿ではiOSアプリ内課金の設計指針について、少しでも多くの開発者のお力添えできる記事を目指しながら執筆させて頂きました。 弊社マネーフォワード確定申告チームでは確定申告のペインを一緒に解消する仲間を募集しています。ご興味がある方は是非ご検討ください。
マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。
【サイトのご案内】 ■マネーフォワード採用サイト ■Wantedly ■京都開発拠点
【プロダクトのご紹介】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』