8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Unity IAP 課金実装メモ

Last updated at Posted at 2023-09-04

はじめに

Unity IAPとは

記事の内容

やること

  • AppStore(iOS) / PlayStore(Android) 両方に対応
  • Androidのコンビニ払い対応
  • 消費型商品の購入と消費

やらないこと

  • Codeless IAP
  • クライアントでのレシート検証(CrossPlatformValidate)
  • AppleのAsk-to-buy対応
  • 定期購入、非消費購入の対応
  • 購入のリストア

購入の流れ

  1. 準備
  2. 初期化
  3. 商品の購入
  4. レシート検証
  5. 購入の確認

1. 準備

IDetailedStoreListenerを実装したクラスを用意する。

以下ではDetailedStoreListenerImplとした:

public class DetailedStoreListenerImpl : IDetailedStoreListener

IDetailedStoreListenerIStoreListenerの拡張で、以下のメソッドが追加されている:

public interface IDetailedStoreListener : IStoreListener
{
    void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription);
}

IStoreListenerOnPurchaseFailedは既にObsoleteなので、前述のものを利用する。

public interface IStoreListener
{
    [Obsolete("Use IDetailedStoreListener.OnPurchaseFailed for more detailed callback.", false)]
    void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason);
}

初期化成功時にIStoreControllerIExtensionProviderが返ってくるので、DetailedStoreListenerImpl で保存しておく。

void OnInitialized(IStoreController controller, IExtensionProvider extensions);
public IStoreController StoreController { get; private set; } = null;
public IExtensionProvider ExtensionProvider { get; private set; } = null;
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
    StoreController = controller;
    ExtensionProvider = extensions;
}

2. 初期化

UnityIAPを使用するために必要な初期化は2段階ある:

1. Unity Gaming Serviceの初期化

// Unity Gaming Serviceの初期化
await UnityServices.InitializeAsync();

2. Unity InAppPurchaseの初期化

// StoreListenerの作成
var storeListener = new DetailedStoreListenerImpl();            
// Buidlerの作成
var module = StandardPurchasingModule.Instance();
var builder = ConfigurationBuilder.Instance(module);
var googleConfig = builder.Configure<IGooglePlayConfiguration>();
googleConfig.SetDeferredPurchaseListener(product => storeListener.OnPurchaseDeferred(product));
// 商品の登録
foreach (var id in productIds) builder.AddProduct(id, ProductType.Consumable);
// IAPの初期化
UnityPurchasing.Initialize(storeListener, builder);

StoreListenerは、前述したIDetailedStoreListenerを実装したクラスのインスタンス(=DetailedStoreListenerImpl)が必要。

Builderには、遅延購入時にStoreListenerOnPurchaseDeferredを発火させるよう設定してをセット。

その後商品情報を登録。消費型のためProductTypeConsumableとする。

そしてStoreListener/Builderの両方を使ってIAPを初期化ができる。

成功するとOnInitializedコールバックが返るので、IStoreControllerIExtensionProviderを保存しておく。

public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
    this.StoreController = controller;
    this.ExtensionProvider = extensions;
}

3. 商品の購入

初期化時に得たIStoreControllerに対し、Productを渡して商品を購入する。

this.StoreController.InitiatePurchase(product);

これを実行すると端末のネイティブ課金UIが出る。

成功すると、IDetailedStoreListener.ProcessPurchaseがコールバックとして呼ばれる。

この後レシートを検証する必要があるため、ProcessPurchase内では一旦PurchaseProcessingResult.Pendingを返す。

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
{
    // ~中略~

    return PurchaseProcessingResult.Pending;
}

4. レシート検証

レシートの内容をAPサーバに送り、そこから各ストアに送信して検証する。

当記事では詳説しない。

Appleの場合

UntiyEngine.Purchasing.Productreceiptstringだからといって直接AppStoreに送らない。
実際にAppStoreが求めているreceipt-datareceiptの中のPayload部分のみなので、jsonを分解して渡す。(1ミス)

    var r = JsonUtility.FromJson<AppleReceipt>(Product.receipt);
    [Serializable]
    public class AppleReceipt
    {
        public string Payload;
        public string Store;
        public string TransactionID;
    }

receipt-data
(Required) The Base64-encoded receipt data.

ちなみにverifyReceiptエンドポイントはもう[Deprecated]なので、APサーバもリプレイス頑張ろう。

5. 購入の確認

レシートの妥当性が検証されたら、購入を確認(Confirm)し、消耗品を消費する。

this.StoreController.ConfirmPendingPurchase(product);

その他処理

購入のキャンセル

購入のネイティブUIをユーザがキャンセルすると、PurchaseFailureReason.UserCancelledを持ったPurchaseFailureDescriptionを引数として、IDetailedStoreListenerOnPurchaseFailedが呼び出される。

public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
    if (failureDescription.reason == PurchaseFailureReason.UserCancelled)
    {
         // UserCancelled
    }
}

購入の失敗

購入が失敗すると、PurchaseFailureReason.PaymentDeclinedを持ったPurchaseFailureDescriptionを引数として、IDetailedStoreListenerOnPurchaseFailedが呼び出される。

public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
    if (failureDescription.reason == PurchaseFailureReason.PaymentDeclined)
    {
         // PaymentDeclined
    }
}

コンビニ払い

Androidのコンビニ払いに対応する。

遅延された購入の通知

初期化で示した例では、以下のように遅延購入のコールバックを登録したが、ユーザーに通知する必要がなければ特に行わなくてもよい。

googleConfig.SetDeferredPurchaseListener(product => storeListener.OnPurchaseDeferred(product));

OnPurchaseDeferredProductを引数に取るが、このProducthasReceiptfalseである(※1)。ダイアログなどでユーザーにコンビニ払いを促す。

public void OnPurchaseDeferred(Product product)
{
    Dialog.Show("コンビニ支払いしてください。");
    // Debug.Log($"コンビニ払いを選択しました: {product.definition.id}");
}

※1 GPBLのPurchaseStateにはPendingがあるのでPendingでレシートを付けてくれると嬉しいのだが…

※2 上記ドキュメントにPendingConstant Value: 2 (0x00000002)と記載があるが、実際にPending状態のProductのレシートを参照するとPurchaseState4になっている。

遅延購入の購入処理

コンビニ払いが行われたならば、よきタイミングProcessPurchaseが発火し、通常購入と同じように購入処理がなされる。基本的に予見できないので、いつProcessPurchaseが発火しても良いように設計しておく。

大きくパターン分けをすると「再起動した場合」「起動したままの場合」に分けられると思われるが、いずれの場合もProcessPurchaseをトリガーに検証&付与のフローが適切に実装されていれば、あまり問題になることはない。ただ購入フローの最後に「◯◯を付与しました」のようなダイアログを出すようになっている場合は、いつ出るかタイミングが掴めない以上、少し検討が必要かもしれない。

コンビニ払いのキャンセル

コンビニ支払いを選択した後にPlayストアで手動でキャンセルしたり、コンビニ払いを期限までに行わなかったなどの理由で自動でキャンセルした場合、キャンセルされた購入のProcessPurchaseは呼ばれることはない。Pending状態のレシートをサーバに送ったりしている場合、サーバ側では期限切れを判断してPendingレシートを消去するなどの処理が必要である。

コンビニ払いの未消費

コンビニ払いをしたのに、その後アプリを72時間起動せずProcessPurchaseで消費されなかった場合、キャンセル扱いになる。この場合のキャンセルはPlayストアの残高に充当される。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?