iOSアプリにオファーコード機能を実装したときの話

こんにちは。NewsPicksエンジニアの takaaki.saito です。
所属しているGrowthチームでは、サービスのGrowth施策を技術面から支えるお仕事をしています。

今回はiOSアプリにオファーコード機能を実装したときのことを振り返り、実装を進める上で工夫したことやハマりどころ等をご紹介します。

実装した当時は、公式ドキュメント以外の情報が少なく、本筋とは別のところでつまづくことがあり苦労しました。本実装を通じて得た知見が、少しでもお役に立てば幸いです!

そもそもオファーコード機能とは

オファーコード機能は、iOSアプリで利用可能なクーポン機能です。

一回限り使用可能な英数字コード(オファーコード)を、AppStoreまたはアプリ内で使用することにより、ユーザは一定期間、無料、または割引価格で有料プランの購読が可能となります。

Appleのアプリ内課金では、もともとプロモーションオファーという別の機能がありますが、これと比較すると、新規ユーザに対してもオファーの提示が可能となったり、メールやチャットツールを通じてコードの配信(オファーの提示)が可能となったりと、ユーザへの訴求面において、オファーコードは汎用性がより高いものとなっています。

※ 比較の詳細は下記URLをご参照ください
https://developer.apple.com/jp/app-store/subscriptions/#offer-types

オファーコードを使用した購読処理の全体フロー

前段で少し触れましたが、オファーコードを使用した購読には2つの方法があります。

  • アプリ内でコードの入力・引換えを行う方法
  • コード引換えURLを利用して、アプリを経由せず、AppStoreにて直接コード引換えを行う方法

各パターンの全体フローは下記のようになります。

アプリ内引換えパターン

コード引換え画面では、ユーザがコードを手入力する必要があります。

コード引換えが完了した時点では、NewsPicksサーバの購読情報(DB)が更新されていないため、アプリ上はオファーが適用されていない状態です。コード引換えの結果をアプリへ反映させるためには、購読画面において購読情報をリストア(端末に保持されているレシートをもとに購読状態を復元する処理)する必要があります。

f:id:rmn-lvrrrr:20210727054212p:plain
全体フロー_アプリ内引換えパターン

アプリ非経由パターン

コードをAppStoreConnectのURLにパラメータとして付与することによって、1回限りのコード引換えURLが作成可能です。 こちらのURLにアクセスするとAppStoreアプリが起動し、そのままAppStoreアプリ内でコード引換えを行います。
アプリ内引換えパターンと比較すると、ユーザ視点ではコードの手入力を省ける利点があります。

※コード引換えURLは下記のような形式をとります
https://apps.apple.com/redeem?ctx=offercodes&id=YourAppAppleID&code=OfferCode

f:id:rmn-lvrrrr:20210727054330p:plain
全体フロー_アプリ非経由パターン

必要な実装

それでは実際に実装していきます。
オファーコード機能を実現するためには、サーバサイド、およびアプリの実装が必要です。
それぞれコード例を交えて説明していきます。

サーバ

offer_code_ref_name

その購読がオファーコードによるものかを判別するためには、レシート検証のレスポンスに含まれる offer_code_ref_name を参照します。 offer_code_ref_nameには、その購読に適用されたオファー名が設定されます(オファーコード不使用の場合は空になります)。

offer_code_ref_nameについて、サーバ側では下記の対応が必要です。

  • レシート検証API (アプリからサーバへレシート検証結果の問い合わせを行うAPI) のレスポンスにoffer_code_ref_nameの追加
  • 課金レシートを管理するテーブルに、offer_code_ref_nameカラムの追加

アプリ

コード引換え画面の表示

こちらの実装は非常に単純です。
任意の場所で presentCodeRedemptionSheet を呼び出すだけで事足ります。
オファーコードはiOS 14.0以降で動作するので、アプリのサポートバージョンが14.0を下回る場合は、下記のように available を使用して要求バージョンを指定する必要があります。
余談になりますが、プロモーションオファーを実装したときは画面(StoryboardやVC)やそこで必要なAPIを自前で作る必要がありましたが、オファーコードは関連ロジック(画面含む)がすべてStoreKitに内蔵されているため、実装が非常に簡略化されました。

import StoreKit
・
・
( 省略 )
・
・
if #available(iOS 14.0, *) {
    SKPaymentQueue.default().presentCodeRedemptionSheet()
}

f:id:rmn-lvrrrr:20210730102534p:plain
コード引換え画面

オファーコードによる購読を検知する

購読アクションの検知は、基本的なアプリ内課金と同様に SKPaymentTransactionObserver を利用します。 今回は購読を検知したタイミングでレシート検証を実行するようにして、その購読がオファーコードを使用したものだった場合は、コード引換え完了ダイアログ(購読画面への導線)を表示するようにしています。

import StoreKit

final class OfferCodePaymentTransactionObserver: NSObject, SKPaymentTransactionObserver {
・
・
( 省略 )
・
・
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchased:
                // 購読トランザクションを完了
                queue.finishTransaction(transaction)
                // レシート検証
                loadAppStoreReceipt()
            case .failed:
                queue.finishTransaction(transaction)
            default:
                break
            }
        }
    }

    private func loadAppStoreReceipt() {
        // レシート検証APIをコールする
        verifyReceiptApi(
            success: { receipt in
                // offerCodeRefNameの存在チェック
                guard let receipt = receipt,
                      receipt.status == .active,
                      let _ = receipt.offerCodeRefName else {
                    return
                }
                // コード引換え完了ダイアログを表示
                showOfferCodeSubscriptionCompleteDialog()
            },
            failure: { _ in
                // do nothing
            }
        )
    }
}

f:id:rmn-lvrrrr:20210730103033p:plain
コード引換え完了ダイアログ

初回起動時にレシート検証を実行する

アプリ非経由パターンでは、アプリ外でコード引換えが完了するため、アプリを起動したタイミングで、最新の購読状態をレシート検証の結果から取得する必要があります。レシート検証は結構重いので、サーバへの負荷増大を避けるため、初回起動時のみ実行するように工夫しています。

class FirstLaunchViewController: UIViewController {
    ・
    ・
    ( 省略 )
    ・
    ・
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        showLaunchDialog()
    }

    private func showLaunchDialog() {
        ・
        ・
        ( 省略 )
        ・
        ・
        // 初回起動時のみ実行
        if needsShowSubscriptionRestorableDialog() {
            // userDefaultsのダイアログ表示フラグを更新
            disableShowSubscriptionRestorableDialog()
            // レシート検証
            loadAppStoreReceipt()
        }
    }

    private func loadAppStoreReceipt() {
        // レシート検証APIをコールする
        verifyReceiptApi(
            success: { receipt in
                // 購読中の課金レシートが存在するか判定
                guard let receipt = receipt,
                      receipt.status == .active else {
                    return
                }
                // リストア促しダイアログを表示
                showSubscriptionRestorableDialog()
            },
            failure: { _ in
                // do nothing
            }
        )
    }
}

ハマりどころ

Sandbox環境で動作確認できない問題

Appleのアプリ内課金では、開発用にSandboxアカウントを利用できます。
オファーコードの開発で最も苦労したのは、このSandboxを利用して動作確認ができない点です。
Sandboxが利用可能な検証用アプリでコード引換えを実行しようとすると、本番アプリのインストールを要求されてしまいます (常に本番に向いてしまうみたいです)。

※下記は当該事象に関連するdeveloper forumのスレッドになります。2021年8月現在においては、まだ未対応のようです。
developer.apple.com

実際には、全体を通しての動作確認は本番環境を使って行いました。
実装による影響範囲が限定的だったというのもありますが、今思い返してもヒヤヒヤしますね。

おわりに

オファーコードは実装自体は比較的シンプルですが、動作確認が困難という特徴がありました。
オファーコード機能の導入を検討される方に、この記事が少しでもお役に立てば幸いです。

© Uzabase, Inc. All rights reserved.