« 【#UWP】 アプリが最初にインストールされた日時を取得する | トップページ | 【#UWP】入門書 「UWP アプリ開発 101」 第2版 (VS2017 対応) 好評発売中です »

2017年4月 4日 (火)

【#UWP】 アプリ内購入 (IAP、あるいは「アドオン」) を実装する [Windows 10 全バージョン対応]

以前は 「アプリ内購入」 (IAP) と呼ばれてました。 今は、アドオンと呼ぶらしい。
ようするに、 アプリ内でのアイテム課金です。

この IAP は、 大きく分けて 2 種類あります。 永続型と消尽型 (コンシューマブル) です。

ここでは永続型の IAP の実装方法を解説していきます。

【目次】

  1. IAP を Windows ストアに申請
  2. 実装の概要
  3. IAP 購入の有無で処理を分岐させる
  4. IAP 購入の有無をシミュレートする
  5. ユーザーに IAP を購入してもらう
  6. まとめ


■ IAP を Windows ストアに申請

まずは、 デベロッパーセンターのダッシュボードで IAP の申請をします。

docs.microsoft.com の「アドオンの申請」 ページを読めば、 だいたい分かると思います。

申請事項のうち、 次のものだけがストア (および、 購入時の UI) で表示されるようです。

  • 価格 (基本価格または地域別の特別価格)、 および、 セール価格 (申請ページでは「販売価格」)
  • タイトル

申請事項の 「説明」 やアイコンなどは、 どこにも表示されません。 ので、 IAP の内容をユーザーに提示するには、 アプリのコードで 「説明」 やアイコンなどを取得 (後述) して表示します。

なお、 IAP を実装したアプリをストアに提出するまでは、 「配布と表示」 (申請ページでは「分布と認知度」) を [購入可能。アプリのリストには表示されません] にしておくと良さげです。

※ アプリがストアで公開されていないと、 IAP の情報をストアから取得できないと思います。 とりあえず他からは見えないように公開しちゃえば良いです。


■ 実装の概要

大きく分けて、 2つあります。

1) 起動時等に IAP 購入の有無で処理を分岐させる
2) ユーザーに IAP を購入してもらう

このうち 2) の方は、 問答無用で購入ダイアログを出しちゃうなら別ですが、 その前に値段などの情報を提示するのがちょっと面倒です。

そして、 実装に使える API が、 2 種類あります! ( ゚Д゚)

Windows 10 の最初のバージョン (1507) から使える: Windows.ApplicationModel.Store
Windows 10 Anniversary Update (1607) から使える: Windows.Services.Store

ここでは、 Windows 10 の全バージョンで使える Windows.ApplicationModel.Store 名前空間の API を使っていきます。
※ 1607 以前を対象にしないアプリなら、 Windows.Services.Store 名前空間を使ったほうがずっと楽そうです。


■ IAP 購入の有無で処理を分岐させる

IAP 購入の有無を調べるには、 LicenseInformation オブジェクトの ProductLicenses コレクションを見れば OK です。

public static LicenseInformation GetLicenseInformation()
#if DEBUG
  => CurrentAppSimulator.LicenseInformation;
#else
  => CurrentApp.LicenseInformation;
  // This API does not require a network connection
#endif


LicenseInformation オブジェクトの取得 (上のコード) は、 インターネット接続を必要としません。 ので、 いつでも参照できると考えて大丈夫です。

そして、 LicenseInformation オブジェクトの ProductLicenses コレクション (読み取り専用の Dictionary です) に、 IAP の製品 ID (次のコードでは purchaseId) をキーとして IAP ごとの  ProductLicense オブジェクトが入っています。

IAP 購入の有無を調べるだけなら、 ProductLicense オブジェクトの IsActive プロパティを見るだけで OK です。

public bool HasPurchase(string purchaseId)
{
  return GetLicenseInformation().ProductLicenses[purchaseId].IsActive;
}


■ IAP 購入の有無をシミュレートする

上の HasPurchase メソッドは、 これだけだと Debug ビルド時は常に false を返してきます (ストアで公開済みなら、 リリースビルドの方は正しく動く)。

アプリをストアで公開する前は、 どうやってテストしましょうか? あるいは、 処理の分岐を繰り返しテストするにはどうしたらいいでしょうか?

それには、 CurrentAppSimulator にテスト用のデータを喰わせます。

CurrentAppSimulator を呼び出すコードを動かすと、 Local ストレージに xml ファイルが生成されるはずです。 それをコピーしてきて書き換えたものを、 CurrentAppSimulator に読み込ませます。

自動生成されるファイル名: WindowsStoreProxy.xml
そのパス: C:\Users\{ユーザー名}\AppData\Local\Packages\{アプリのパッケージファミリ名}\LocalState\Microsoft\Windows Store\ApiData

WindowsStoreProxy.xml の内容は、 ↓こんなふうになってます。

<?xml version="1.0" encoding="utf-16" ?>
<CurrentApp>
    <ListingInformation>
        <App>
          ……中略……
        </App>
        <Product ProductId="1" LicenseDuration="0" ProductType="Durable">
            <MarketData xml:lang="en-us">
                <Name>Product1Name</Name>
                <Price>1.00</Price>
                <CurrencySymbol>$</CurrencySymbol>
                <CurrencyCode>USD</CurrencyCode>
            </MarketData>
        </Product>
        <Product ProductId="2" LicenseDuration="0" ProductType="Consumable">
          ……中略……
        </Product>
    </ListingInformation>
    <LicenseInformation>
        <App>
            <IsActive>true</IsActive>
            <IsTrial>true</IsTrial>
        </App>
        <Product ProductId="1">
            <IsActive>true</IsActive>
        </Product>
    </LicenseInformation>
    <ConsumableInformation>
      ……中略……
    </ConsumableInformation>
</CurrentApp>


永続型の IAP の場合は、 ProductId="1" の「1」を製品 ID に書き換えます (2箇所)。 そして、 下のほうの <IsActive> 要素の true / false で、 IAP 購入の有無を指定します。

この xml ファイルを、 アプリの実行中に CurrentAppSimulator の ReloadSimulatorAsync メソッドを呼び出して読み込ませます。

なお、 動的に xml ファイルを生成する場合は、 UTF-8 じゃなくて UTF-16 (BE) で書き出さないといけないので注意してください。 また、 シビアに大文字 / 小文字が区別されるので、 それも気をつけてください。
# <IsActive> 要素に 「True」 って書いたら、 怒られた… orz

xml データを Temp ストレージに書き出し、 それをシミュレータに喰わせるコードは次にようになります。

#if DEBUG
public async Task DEBUG_ReloadSimulatorAsync(string xmlData)
{
  const string xmlFileName = "……";
  var tempFolder = Windows.Storage.ApplicationData.Current.TemporaryFolder;
  var xmlFile = await tempFolder.CreateFileAsync(xmlFileName, Windows.Storage.CreationCollisionOption.ReplaceExisting);
  await Windows.Storage.FileIO.WriteTextAsync(xmlFile, xmlData, Windows.Storage.Streams.UnicodeEncoding.Utf16BE);

  await Windows​.ApplicationModel​.Store.CurrentAppSimulator.ReloadSimulatorAsync(xmlFile);
}
#endif


■ ユーザーに IAP を購入してもらう

いきなり購入の UI を出してもビックリされちゃいますから、 まずは IAP の情報を提示しましょう。

IAP の価格や説明の情報は、 ストアから ProductListing オブジェクトを貰ってくると、 そこに入っています。 ProductListing オブジェクトは、 ListingInformation オブジェクトに入ってます。 ListingInformation オブジェクトは、 CurrentApp の LoadListingInformationAsync メソッドで取得できます。

LoadListingInformationAsync メソッドは、 ストアと通信しますので、 インターネット接続が必須ですし、 失敗する (ストアから返信してもらえない) こともありえます。
※ 失敗したときは、 そのまま購入 UI を出してもおそらくまた失敗しますから、 「ごめんなさい」 して処理を打ち切ったほうが良いでしょう。

製品 ID (次のコードでは purchaseId) に結びつけられた ProductListing オブジェクトを貰ってくるコードは、 次のようになります。 得られる情報の意味もコメントしておきました。

public static async Task<ProductListing> GetProductListingAsync(string productId)
{
  try
  {
#if DEBUG
    ListingInformation li = await CurrentAppSimulator.LoadListingInformationAsync();
#else
    ListingInformation li = await CurrentApp.LoadListingInformationAsync();
    // Calling this method requires an internet connection.
#endif

    //// アプリの情報
    //uint ageRating = li.AgeRating;  // 年齢レイティング
    //string currencyCode = li.CurrencyCode;  // 通貨 ("JPY" など)
    //string currentMarket = li.CurrentMarket;  // マーケット ("JP" など)
    //string description= li.Description; // アプリの説明
    //string formattedBasePrice = li.FormattedBasePrice;  // アプリの基本価格 ("¥0" など)
    //string formattedPrice= li.FormattedPrice; // アプリの現在の価格 ("¥0" など)
    //bool isOnSale= li.IsOnSale; // 特売期間中か?
    //string name = li.Name;  // アプリの名前
    //DateTimeOffset saleEndDate =li.SaleEndDate; // 特売期間の終了予定日時

    IReadOnlyDictionary<string, ProductListing> productListings = li.ProductListings;
    //foreach (var p in productListings)
    //{
    //  ProductListing pl = p.Value;

    ////  IAP の情報
    //  string currencyCode1 = pl.CurrencyCode; // 通貨 ("JPY" など)
    //  string description1 = pl.Description; // IAP の説明
    //  string formattedBasePrice1 = pl.FormattedBasePrice; // IAP の基本価格 ("¥0" など)
    //  string formattedPrice1= pl.FormattedPrice;  // IAP の現在の価格 ("¥0" など)
    //  Uri imageUri= pl.ImageUri;  // アイコン (300x300) の Uri
    //  bool isOnSale1 = pl.IsOnSale; // 特売期間中か?
    //  List<string> keywords = pl.Keywords.ToList(); // キーワードのリスト
    //  string name1 = pl.Name; // IAP の名前
    //  string productId1 = pl.ProductId; // 製品 ID
    //  ProductType productType = pl.ProductType; // IAP の区分 (Durable or Consumable)
    //  DateTimeOffset saleEndDate1 = pl.SaleEndDate; // 特売期間の終了予定日時
    //  string tag = pl.Tag;  // カスタムの開発者データ
    //}

    if (productListings.ContainsKey(productId))
      return productListings[productId];
  }
  catch { }
  return null;
}


上の ProductListing オブジェクトから適切な情報をユーザーに提示してから、 購入 UI を表示します。

20170402_iap05b_2
※ FormattedPrice (IAP の現在の価格) を表示しています (赤丸内)
※ 初回ダウンロード日の取得方法は前記事 「【#UWP】 アプリが最初にインストールされた日時を取得する」 を参照


そうそう、 購入 UI を表示しただけでは購入したことになりません。
ので、 [購入] なんてそっけないボタンではなく、 [購入に進む] とか [購入手続きに入る] とかいったボタンにしたほうが良いでしょう。 このボタンを押しても 「まだ引き返せるよ!」 って分かるように。

購入 UI を表示するのは、 次の 1 行だけです。

await CurrentApp.RequestProductPurchaseAsync(productId);

※ これ↑もネットワークが繋がっていないと例外を吐きます。

20170402_iap06
※ この購入 UI (右の大きいダイアログ) は UWP 側が出しています。 ユーザーの操作が完了すると ( [了解] ボタン=購入した or [キャンセル] ボタン)、 制御がコードに返ってきます。
※※ この購入 UI ってば、 こんなにスペースを余らせてるのに、 表示してるのはタイトルと価格だけ。 説明やアイコンも表示してくれればいいのに…… (--;


そして、 購入 UI から帰ってきたら、 「IAP 購入の有無で処理を分岐させる」 で説明したようにして、 再び LicenseInformation を取得します (RequestProductPurchaseAsync メソッドの返値は役に立ちません)。

あるいは、 LicenseInformation オブジェクトの LicenseChanged イベントを待ち受けておいて、 そのハンドラー内で LicenseInformation を取得しても良いです。

そうすると、 他のデバイスで購入してもローミングされて呼び出してもらえるというメリットがあります (念のために両方、 というのもありかもしれない)。

なお、 このイベントは本番では別スレッドから叩かれますので要注意! (本番の購入 UI でキャンセルしても、 このイベントは発生します)。


■ まとめ

Win10 全バージョン向けに、 永続型 IAP を購入するための実装をざっくり説明してみました。

MSDN や docs.microsoft.com を読んでみても、 要領を得ないんですよねぇ。
おまけに、 シミュレータと本番では得られるデータの細部が違ってたり、 スレ違いだったり… (--;

なので、 可能な限り本番を使って実装を進めることをお勧めします。
また、 最後の最後に (ストアで IAP 価格を 0 円にしておいて) 実際に購入してみるテストもやっちゃいましょう! (それでバグを見つけちゃったら、 その IAP は非表示にして新しく IAP を申請しちゃえばいいから f(^^;)
# 実際、 バグじゃないけど仕様漏れを見つけちゃった… (汗;

※ 本記事は UWP 汎用の話ですが、 上に載せた画像のアプリは Xamarin.Forms (PCL) 製です。 Xamarin でプラットフォーム固有の実装をする方法はいくつかありますが、 ここでは DependencyService を使っています。

|

« 【#UWP】 アプリが最初にインストールされた日時を取得する | トップページ | 【#UWP】入門書 「UWP アプリ開発 101」 第2版 (VS2017 対応) 好評発売中です »

* プログラミング ( Metro スタイル )」カテゴリの記事

プログラミング」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)


コメントは記事投稿者が公開するまで表示されません。



トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/209349/65108262

この記事へのトラックバック一覧です: 【#UWP】 アプリ内購入 (IAP、あるいは「アドオン」) を実装する [Windows 10 全バージョン対応]:

« 【#UWP】 アプリが最初にインストールされた日時を取得する | トップページ | 【#UWP】入門書 「UWP アプリ開発 101」 第2版 (VS2017 対応) 好評発売中です »