1
/
5

【開発日誌#29】iOS 自動更新サブスクリプションの実装について

はじめに

iOSには課金の種類が数種類あることをご存知でしょうか?
今回は課金の種類を簡単にご紹介した上で、iOS自動更新のサブスクリプションについて、
ご紹介したいと思います!

1.概要

Appleの全てのプラットフォームでアプリ内課金により、デジタルグッズ、定期購読、プレミアムコンテンツ等の追加コンテンツや機能をアプリ内で直接提供することができます。


2.アプリ内課金の種類について

アプリ内課金には4つの種類が存在し、アプリ内で複数の種類を提供することができます。

①消費型アプリ内課金
 ゲーム内でのスタミナやガチャを引くためのアイテムなど。
 消費型アプリ内課金は、使用することで消耗し、再度購入することができます。

②非消費型アプリ内課金
 広告の非表示や使える機能の拡張など。
 一度購入すれば、無制限に使用することができます。

③自動更新のサブスクリプション
 月額サービスのクラウドストレージや週間雑誌のサブスクリプションなど。
 ユーザーが解約するまで、定期的に課金されます。

④非更新型サブスクリプション
 ゲーム内コンテンツのシーズンパスなど。
 自動更新されないため、ユーザー自身が毎回更新する必要があります。


3.自動サブスクリプションの実装について

1.プロジェクトの設定

XCodeのプロジェクト内のターゲットを選択し、「Team」を正しいチームに切り替え、作成したバンドルIDを入力します。

次に「Capability」タブをクリックして、「In-App Purchase」のスイッチをOnにします。


2.コードの実装

・StoreKITをimportする

import StoreKit

・すべての商品の購入をリクエストする

private let productIdentifiers: Set<ProductIdentifier>
private var purchasedProductIdentifiers: Set<ProductIdentifier> = []
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    print("Loaded list of products...")
    let products = response.products
    productsRequestCompletionHandler?(true, products)
    clearRequestAndHandler()

    for p in products {
      print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
    }
  }
  
  public func request(_ request: SKRequest, didFailWithError error: Error) {
    print("Failed to load list of products.")
    print("Error: \(error.localizedDescription)")
    productsRequestCompletionHandler?(false, nil)
    clearRequestAndHandler()
  }

  private func clearRequestAndHandler() {
    productsRequest = nil
    productsRequestCompletionHandler = nil
  }
}

・購入する

public func buyProduct(_ product: SKProduct) {
  print("Buying \(product.productIdentifier)...")
  let payment = SKPayment(product: product)
  SKPaymentQueue.default().add(payment)
}

・応答Delegate

この拡張機能は、SKProductsRequestDelegateプロトコルで要求される2つのメソッドを実装することにより、Appleのサーバーから商品のタイトル、説明、価格を取得するために使用されます。

// MARK: - SKPaymentTransactionObserver
 
extension IAPHelper: SKPaymentTransactionObserver {
 
  public func paymentQueue(_ queue: SKPaymentQueue, 
                           updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
      switch transaction.transactionState {
      case .purchased:
        complete(transaction: transaction)
        break
      case .failed:
        fail(transaction: transaction)
        break
      case .restored:
        restore(transaction: transaction)
        break
      case .deferred:
        break
      case .purchasing:
        break
      }
    }
  }
 
  private func complete(transaction: SKPaymentTransaction) {
    print("complete...")
    deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
  }
 
  private func restore(transaction: SKPaymentTransaction) {
    guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
 
    print("restore... \(productIdentifier)")
    deliverPurchaseNotificationFor(identifier: productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
  }
 
  private func fail(transaction: SKPaymentTransaction) {
    print("fail...")
    if let transactionError = transaction.error as NSError?,
      let localizedDescription = transaction.error?.localizedDescription,
        transactionError.code != SKError.paymentCancelled.rawValue {
        print("Transaction Error: \(localizedDescription)")
      }

    SKPaymentQueue.default().finishTransaction(transaction)
  }
 
  private func deliverPurchaseNotificationFor(identifier: String?) {
    guard let identifier = identifier else { return }
 
    purchasedProductIdentifiers.insert(identifier)
    UserDefaults.standard.set(true, forKey: identifier)
    NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
  }
}

・サーバーで検証する

func verifyReceipt(completion: ((Bool) -> Void)? = nil) {
        guard !isVerifying else { return }
        guard let _ = UserSessionManager.shared.getUserId(),
              let receiptData = getLocalReceipt(),
              let udid = UIDevice.current.identifierForVendor?.uuidString else { completion?(false); return }
        
        self.isVerifying = true
        let request = VerifyRecepitRequest(receipt_data: receiptData, udid: udid)
        APIService.verifyReceipt(request)
            .subscribe(onNext: {[weak self] in completion?($0.isSuccess) ;  self?.isVerifying = false },
                       onError: {[weak self] err in
                print("verifyReceipt \(err.localizedDescription)")
                completion?(false)
                self?.isVerifying = false
                DispatchQueue.main.async {
                    let isToLogin = err.codeData == 403
                    self?.showAlert(message: isToLogin ? R.string.localizable.login_again_title() : err.messageData,
                                    buttonTitle: isToLogin ? R.string.localizable.to_login_title() : "OK",
                                    completion: {
                        if isToLogin {
                            UserSessionManager.shared.logoutHandler()
                        }
                    })
                    self?.removeAll()
                }
            }).disposed(by: self.bag)
    }

・ローカルレシートを要求する

private func getLocalReceipt() -> String? {
        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
            FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {

            do {
                let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
                return receiptData.base64EncodedString()
            } catch { print("Couldn't read receipt data with error: " + error.localizedDescription)
                return nil
            }
        } else {
            return nil
        }
    }

3.Sandboxでの購入テスト

アプリをビルドして実行しますが、購入をテストするには、実機で実行する必要があります。事前に作成したサンドボックステスターを使えば、課金されずに購入を実行することができます。iPhoneの設定画面にアクセスし、通常のApp Storeアカウントからログアウトしていることを確認し、「iTunes&App Store」から購入のテストができます。

以上で終了となります!

上記は某ミュージシャンとファンが交流できるようなファンクラブ限定のSNSアプリで、開発時にサブスクリプションの機能が必要だったため、簡単にご紹介させていただきました!

他にも不動産ポータルアプリやAIを用いたアプリ、カタログアプリ、モバイルオーダーアプリ、SNSアプリなど多岐に渡るアプリを開発しております。

いかがでしたでしょうか?上記のような課金機能実装のアプリ開発に興味がある方や、アプリ開発の知見をさらに深めていきたい方はぜひご応募お待ちしております!


コムデが発信する記事や写真等の無断転用はお断りしています。Wantedlyコンテンツの使用・引用をご希望の場合は一度弊社までお問い合わせください。

株式会社コムデからお誘い
この話題に共感したら、メンバーと話してみませんか?
株式会社コムデでは一緒に働く仲間を募集しています
8 いいね!
8 いいね!

同じタグの記事

今週のランキング

竹辻 篤志さんにいいねを伝えよう
竹辻 篤志さんや会社があなたに興味を持つかも