ラベル アプリ内課金 の投稿を表示しています。 すべての投稿を表示
ラベル アプリ内課金 の投稿を表示しています。 すべての投稿を表示

2013年7月22日月曜日

Google Play In-App Billing テスト方法

続きの続き。Google PlayのIAB(In-App Billing)のテストの話を乗せときます。
あまり載せることがないので、基本的には オフィシャル の内容ほぼそのままです。

Google Playのアプリ内課金テストではAmazonのIAPのようなテストアプリは使いません。同様の機能をPlayStoreが既に備えています。また、本番環境でのテストをする方法も既に用意されています。

2つのテスト方法
Google Play には、アプリ内課金の実装テストに役立つ2つの機能が用意されています。
  1. サンドボックステスト
    • 予めテスト用に予約されたアイテムIDを使って、所定のレスポンス時のテストを行える。
  2. 実購入テスト
    • Google Playを通じて実際の購入と同じ環境でアプリ内課金を行うことができる。こちらはGoogle Playへの通信を行い、実際にGoogle Walletへの取引が発生する。尚、テストではこの課金はキャンセルされ実際の請求は発生しない。

どちらのテスト方法もアプリを公開する前に行うためのテストですが、それぞれ役割が違います。
  • サンドボックステストは、主にアプリ内課金機能の単体テストレベルで利用します。
  • 実購入テストは、公開前の結合・システムテストレベルで行うテストに利用します。

この2つのテスト方法を利用するには以下の条件があります。
  • テストにはPlayStoreアプリが必要になるため、実端末でしか行えない。
    (エミュレータ不可)
  • 端末はAndroid 1.6以上かつ、PlayStoreアプリのVersion 2.3.4以上が必要。
    Android 3.0の場合は、MyApps Version 5.0.12以上が必須環境となります。

テストできるアプリ内課金種類
 テストとして購入できるアプリ内アイテムは、「アプリ内管理アイテム(買切り型アイテム)」と、「消費するアプリ内管理アイテム」の2つです。「Subscription」のテスト購入はできません...残念。

サンドボックステスト
 予約されている所定のアイテムIDを使ってアプリ内課金処理の実装をテストできます。このテストでは、実装が正しくGoogle Playからのレスポンスを処理し、署名を検証できることを確認できます。

各予約済みアイテムIDを使用して購入リクエストを行うと、Google Playがそれぞれ特定のレスポンスを返してくれます。もちろん課金は発生しません。尚、このテスト方法では支払方法を指定することはできません。
また、サンドボックステストを行うためにAPKをアップロードしたり、アプリ内で販売するアイテムリストを公開する必要はありません。

予約済みアイテムID
サンドボックステストでは、以下4つの予約済みアイテムIDを使ってテストをすることができます。
  1. android.test.purchased
    購入成功レスポンスを返します。署名の検証テストのためにJSON文字列とレスポンスに署名が含まれています。
  2. android.test.canceled
    購入処理中にエラーが検知された・購入がキャンセルされた時のレスポンスを返します。これは登録されているクレジットカードが無効だった場合やユーザー操作により購入がキャンセルされた時に返されるレスポンスです。
  3. android.test.refunded
    払い戻されたときのレスポンスを返します。
    払い戻しはGoogle Play In-App Billingサービスから行うことはできません。 払い戻しは、販売者自身のGoogleWalletから行う必要があり、Google Walletから払い戻しが行われた 通知を受けた後、Google Playからアプリへ通知されます。
    払い戻し処理に関する詳細は、「 Handling IN_APP_NOTIFY messages 」と「 In-app Billing Pricing 」を参照してください。
  4. android.test.item_unavaliable
    リクエストされたアイテムIDが、公開しているアプリ内アイテムリストに存在しなかった場合のレスポンスを返します。
 いくつかの予約済みアイテムIDを使用したサンドボックステストでは、レスポンスに署名を返すことができます。レスポンスに署名含まれるパターンは以下の表のとおりです。

過去にAPKを公開済み APKをドラフトアプリとしてアップロード(非公開) アプリを実行しているユーザーアカウント レスポンスに署名が含まれるか?
No No Any 非署名
No No 開発者アカウント 署名
Yes No Any 非署名
Yes No 開発者アカウント 署名
Yes No テストアカウント 署名
Yes No Any 署名

サンドボックステストの手順
 サンドボックステストを行うために特別必要な手順はありません。
  1. テストする端末にアプリをインストールします。
  2. 開発者Google アカウントでログインします。
  3. テストする端末のPlayStoreまたはMyAppsアプリのバージョンが必要なバージョンであることを確認して下さい。
  4. テストするアプリで予約済みプロダクトIDを使用してアプリ内課金を行う。

実購入テスト
 サンドボックステストで動作確認がとれたら、実際にアプリ内購入を行ってテストしましょう。このテストは、アプリをリリースする前に、実際のGoogle Playと通信を行った本番環境で課金を行います。Google Walletにも課金処理が走りますが、予めテストアカウントを指定しておくことにより当該テストアカウントの取引を自動的にキャンセルすることで実際の課金無しに本番環境でテストすることができます。

Note:
実購入を行うテストアカウントは端末上に設定されている必要があります。端末に複数アカウントが設定されていた場合、アプリ内課金を行う際はダウンロードしたアカウントへ課金が発生します。ダウンロードしたユーザーアカウントが端末上に設定されていなかった場合は、メインのユーザーアカウントで購入しようとします。

※ここではGooglePlayのデベロッパーアカウントを持っている必要があります。デベロッパー登録には25US$を支払う必要があります。また支払のためのクレジットカードも必要です。

実購入テストの設定
『テストアカウント』の設定
 公開前のアプリをダウンロードし、実購入テストを行えるアカウントを設定する必要があります。テストアカウントの設定方法はデベロッパーコンソールにて以下の手順で設定します。 In-App Billing V3を利用している場合、ここで設定したアカウントだけがテスト購入できるアカウントとなります。
  1. 左メニューの「Settings」を選択します。
  2. 表示されたページ内の「Account details」を選択します。
  3. 表示されたページ内の「License Testing」セクションにある 「Gmail accounts with testing status」テキストボックスへテストアカウントとして設定するアカウントのGMailアドレスを入力してください。
  4. 入力後は、必ず「Saved」で保存してください。
設定完了後、15分以内に設定内容が反映されます。繁栄されればそのアカウントでドラフトアプリのアプリ内課金が利用可能になります。 ※尚、テストアカウントに開発者アカウントは設定できません。  (GoogleWallet上、自分から購入することができないため。)

Android端末に設定されているメインアカウントの変更方法
 公式的にはファクトリーリセット(工場出荷状態へ戻す)し、セットアップ時またはシステム設定から再度メインアカウントを登録する必要があります。端末によっては異なる方法でもできるという話がありますが、オフィシャル的にはこれが一番?みたいですので面倒でも課金処理周りですから手堅く一度リセットして設定しましょう。

実購入テストの実施
アプリ内課金の購入テストを行うには以下の手順に従ってください。
  1. テスト対象アプリのAPKファイルをデベロッパーコンソールへアップロードします。
    (未公開のままでOK)
    • アップロードするAPKファイルは、リリース用のKeyStoreで署名されている必要があります。
  2. アプリ内で販売するアイテムリストを作成・公開する。
    • アプリ内で販売するアイテムがデベロッパーコンソール上で「公開」状態になっていることを確認して下さい。
  3. Android端末にアプリをインストール
    • デベロッパーコンソールで設定したテストアカウントが設定されている端末へアプリをインストールします。
    • PlayStoreアプリ または MyAppsアプリのバージョンが最新または対応バージョンであることを確認して下さい。
  4. アプリ内でアイテムを購入する。(テストの実施)
※デバイスでテストするAPKとアップロードしたアプリのバージョンNo.は一致している必要があります。


 お金がらみの機能でバグ出すとレビュー欄が荒れるので、本番環境を使ってしっかりテストしたいところ。 欲を言えば、Subscriptionのテストが簡単にできる機能も欲しいところです。あっちのテストの方が色々面倒なんで。

2013年7月12日金曜日

Subscription - In-App Billing

前回の「In-App Billing Version 3 概要」の続きで、In-App BillingにおけるSubscriptionについての記事です。技術的な話というよりもSubscriptionを売るときに考えなきゃならない事がメインです。公式サイトのSubscriptionページを解釈して書いてます。

Subscriptionって?
定期的(毎月または年間)に自動更新され課金が発生するアプリ内課金アイテムの種類です。有料会員用コンテンツへのアクセス権や、期間で課金したい機能などの販売に利用できます。

他のアプリ内課金アイテムと同様にアプリ内にて販売でき、もちろんアプリにて直接取引を行う必要はありません。GooglePlayがGoogleWalletを通して統一された課金手続きにより処理します。これは、ユーザーへ一貫した購入フローを提供するためです。

外部サービスのSubscriptionの使用
アプリ内に既存の外部サービスSubscription加入者を作成することもできます。
  • ウェブサイトなどで既にSubscriptionを販売していて、アプリからそのSubscriptionを購入しているかどうかを判断するためにアプリに独自のビジネスロジックを追加することができます。
  • 必要があれば、多くの異なるアプリ間で共有できる横断的なサブスクリプションも実装できます。例えばあなたの全アプリ、ゲームまたは他のコンテンツへアクセスするための月会費・年会費としてのSubscriptionを販売することも可能です。これを実装するためには、あなたのアプリにて有効なSubscriptionを判断しコンテンツへのアクセスを許可させるビジネスロジックを追加する必要があります。

販売者の責務
サブスクリプションの販売者は、ユーザーが有効なサブスクリプションによってコンテンツ・機能を利用する権利を有している限り該当コンテンツ・機能を提供し続けなければなりません。これを行わなかったり、有効なサブスクリプションやコンテンツ・機能を削除するなどして利用できなくした場合は処罰の対象となります。

詳細については、 公式サイトのポリシードキュメント を参照してください。

アイテムの属性設定
販売するSubscriptionアイテムはデベロッパーコンソールにて設定できます。Subscriptionアイテムには、以下の属性を設定できます。
  • 課金種別:Subscriptionに設定
  • Subscription ID:Subscriptionの識別ID
  • 公開状態:未公開/公開
  • 言語:Subscriptionを表示するためのデフォルト言語
  • タイトル:Subscriptionアイテムのタイトル
  • 説明:Subscrptionの詳細説明
  • 価格:1Subscription当たりの契約価格
  • 期間:毎月 or 毎年
  • 自動更新の可否:可/不可

デベロッパーコンソールのアイテムを追加して構成する方法の詳細は こちらの公式ページ を参照してください。

価格設定
Subscriptionの作成時に任意の通過で価格を設定できます。ただし、各Subscriptionは0以上の価格を設定する必要があります。また、同じコンテンツに対する価格の違う複数のSubscriptionも作成できます。
(例えば、年間購読の場合は毎月よりも割り引かれた価格を設定するなど)

【注意】
 Subscriptionの価格を変更する場合、新しい価格のSubscriptionを新規作成し、アプリでその新しいSubscriptionを提供するようにしてください。そうすることで既に購入しているユーザーは元の価格のSubscriptionで課金されたまま、新しいユーザーは新しい価格のSubscriptionで課金されます。

自動継続
Subscriptionは、デベロッパーコンソールにて以下のどちらかの期間で自動継続を設定できます。
  • 毎月―初回購入後、毎月購入日にSubscriptionを自動継続して課金します。
  • 毎年―初回購入後、毎年購入日と同じ日にSubscriptionを自動継続して課金します。
Subscriptionでは、設定した金額で無期限に自動継続されます。
Subscriptionが継続された時、GooglePlayがユーザーへ課金を行いメールでその旨を通知します。課金期間は購入日に基づいてSubscriptionの設定期間により計算されます。また、自動継続された際の請求は購入時と同じ支払方法になります。(クレジットカード支払やキャリア課金など)

試用期間
タダ(無料)で有料機能をお試しできる期間を設定することができます。
  • Subscriptionへ試用期間を設定するためにアプリを変更する必要はありません。デベロッパーコンソールのSubscriptionアイテムの設定で試用期間を設定することができます。
  • 試用期間を設定する場合、最低でも7日以上の日数を設定する必要があります。
  • 設定した試用期間を過ぎた場合、自動的にそのSubscriptionは設定されている本来の金額で契約されます。
  • 販売者は、いつでもSubscriptionの試用期間を変更できます。しかし、既に試用期間中のSubscriptionを持っているユーザーに対しては期間の変更は適用されません。
  • 試用期間はSubscriptionアイテム毎に個別に設定できます。
試用期間は0円の課金を行うことで実現しています。試用期間中は無料なので請求は発生しませんが、GooglePlayが0円のSubscriptionを購入したことをユーザーへ電子メールで通知します。

ユーザーは必要があれば任意のタイミングでSubscriptionをキャンセルできます。

解約について
In-App Billing APIに解約機能はありません。よってアプリ内からSubscriptionの解約は行えません。Subscriptionの解約は、PlayStoreアプリから行います。

解約されたSubscriptionの扱い

有効期間中にユーザーがSubscriptionを解約しても、払い戻しはされません。
その代りに、キャンセルされても購入したSubscriptionの期間が終了するまでキャンセルされたSubscriptionであっても有効なままとなります。

いくつかのケースでは、解約のためにユーザーが直接連絡する必要があるケースがあります。また、必要に応じてサーバーAPIを使用して直接ユーザーのSubscriptionをキャンセルすることができます。

アプリのアンインストール
ユーザーがサブスクリプションを購入したアプリをアンインストールしようとした場合、PlayStoreアプリが有効なサブスクリプションが存在することをユーザーに通知します。
  • ユーザーがアプリのアンインストールを続行した場合
    • アプリは削除されますがサブスクリプションは有効なまま課金も継続されます。(※アプリのアンインストール時に自動的にサブスクリプションがキャンセルされませんのでご注意ください。)
  • ユーザーがアンインストールをキャンセルした場合
    • アプリとサブスクリプション両方とも残ります。
※ユーザーは、PlayStoreアプリのMy Apps画面よりいつでも関連するサブスクリプションをキャンセルすることができます。

払い戻し
 Google Playでは払い戻し画面を用意していないので、返金を要求する場合は直接連絡を頂く必要があります。払い戻しの要求を受けた場合、そのユーザーのサブスクリプションをキャンセル、または既にキャンセルされている事を確認する為にサーバーAPIを使用できます。

しかし、GooglePlayはキャンセルされたサブスクリプションにおいても期間中はサブスクリプションが有効であるとみなしている事に留意してください。サブスクリプションをキャンセルし払い戻されたユーザーであってもそのキャンセルされたサブスクリプションの期間中はコンテンツへのアクセスができます。

【重要】
キャンセルされたサブスクリプションの一部払い戻しは、現時点で対応していません。


取引手数料
  • 一般にGooglePlayの条件は、GoogleWalletの標準的な支払方法を通じてのみアプリ内サブスクリプションを販売できます。
  • GooglePlayに公開するアプリにおいて、サブスクリプションを販売するためにはIn-app Billingのトランザクションを処理する必要があり、GooglePlayとアプリの外での購入手続きへのリンクを提供することはできません。条件とポリシーに関する詳細については、 公式サイトのポリシー を参照してください。
  • サブスクリプションの購入にかかる取引手数料は一般的なアプリ内課金の手数料(取引金額の30%)と同じです。

注文番号
特定Subscriptionの取引を追跡するために、GoogleWalletは全ての基本マーチャント注文番号を再発行を提供します。 以下のように基本注文番号の末尾に整数を付加することによって、連続したSubscriptionの取引を表します。
12999556515565155651.5565135565155651 (基本販売者注文番号)
12999556515565155651.5565135565155651..0 (初期購入注文ID)
12999556515565155651.5565135565155651..1 (最初の定期的購入注文ID)
12999556515565155651.5565135565155651..2 (2回目の定期購入注文ID)
Google Playの注文コードフィールドに注文番号が含まれています。
In-App Billing Version 3では、INAPP_PURCHASE_DATA JSONフィールドに、Version 2では、PURCHASE_STATE_CHANGED に含まれています。

購入トークン
Subscriptionの支払がGoogle Walletで承認された後、In-App Billing APIを通じてGooglePlayから購入トークンを取得できます。

この購入トークンは、デバイス上に保存したり、バックエンドサーバへ渡すことができ、Google Playが提供しているHTTPベースのAPI "Purchase State API " を利用してバックエンドサーバからリモートでサブスクリプションを検証したり、キャンセルするために利用できます。

以上。

2013年7月4日木曜日

In-App Billing Version 3 概要

いまさら感はありますが、Google Play Store アプリ内課金サービスである In-App Billing Version 3でSubscriptionが対応された為、おさらい的にOverviewから自分なりの解釈を加えて説明資料を残しておきます。

Google Play Store In-App Billing とは
Android アプリに対して課金機能を提供するAPI。
  • Play Store アプリを経由しGoogle Walletを通じて課金処理を行ってくれます。 さらに、アプリ内で購入するための決済処理UIも提供してくれます。
  • 課金する商品(アイテム)の設定を行うための管理機能は、 デベロッパーコンソール( http://play.google.com/apps/publish )にて提供されています。
  • ※ Developer Consol への登録はアプリを公開したりするサイト・サービスです。 登録には25 USドルが必要になります。
Version 3 の新しい機能
  1. 購入情報がキャッシュされるようになった
    • Play Store アプリが端末内で購入情報をキャッシュするようになりました。
      これで、購入情報取得リクエスト時にすごい待たされることもなくなる?
  2. やっと Subscriptionに対応した
    • Version 3 の新機能というよりは、最近(?)やっとついた機能です。
      Version 3 が出て暫く経っても Subscriptionに関してはVersion 2でしか 対応してなかったのですが、対応されたようです。よかったよかった。。。

取り扱える課金種類
In-App Billing サービスを利用して販売できるアイテム種類は以下の3つです。
基本、AmazonのIn-App Purchasingと変わりませんが、少々取扱いが違います。  
  • アプリ内管理アイテム
    1. 非消費型-アプリ内管理アイテム
      買い切り型アイテム。 広告非表示機能とか有料版アップグレードなどの類のもの。 買ったらその機能を使用する権利を永続的に保持できるアイテム。
    2. 消費型-アプリ内管理アイテム
      使ったらなくなるアイテムのことです。 例えば、ゲーム内の通貨や燃料、行うごとに課金されるコンテンツなどが該当します。 アイテム数もGoogle Play サーバ上で購入した数を管理しており、 消費する際には消費することもGoogle Playへ通知する必要があります。
  • サブスクリプション
    毎月または年間期間単位で、アプリ内のコンテンツ、サービス、または機能を 販売することできるアプリ内課金アイテム種別の1つです。
    他のアプリ内管理アイテムと同様に、デベロッパーコンソールから サブスクリプションアイテムを設定して、アプリ内から販売できます。

一般的なアイテム購入までの処理フロー図
公式サイトの説明から少し独自に変えてあります。
  1. バージョン確認リクエスト
    端末にインストールされているPlay StoreアプリがIn-App Billing API Version 3に 対応しているか確認するために、”isBillingSupported()”リクエストを送信します。
    リクエストが成功した場合、サポートしている or 非サポートの情報を返します。

  2. 購入情報取得リクエスト
    ユーザーの購入履歴や購入情報詳細を照会するには、"getPurchases()"リクエストを送信します。 リクエストが成功した場合、Google Playから「レスポンスコード」、「購入済みアイテムのリスト」、 「購入情報リスト」、「署名リスト」、「トークン情報」を含むバンドルを返します。

  3. アイテム詳細取得リクエスト
    デベロッパーコンソールで設定した、アプリ内で購入可能なアイテムの詳細を Google Playを通して取得できます。アイテムのIDを指定して"getSkuDetails()"リクエストを送信し、 成功するとGoogle Playはアイテムの「タイトル」、「説明」、「アイテム種別」、 「価格」を含むバンドルが返します。

  4. 購入リクエスト
    購入手続きUIを起動するためのインテントを取得するため、 購入対象となるアイテムのIDを指定して、"getBuyIntent()"リクエストを送信します。 リクエストが成功するとGoogle Playから、アイテム購入用UIを起動するための Pending Intentを含むバンドルが返されます。

  5. 購入用UI起動~購入手続き
    • [4.購入リクエスト]で取得したアイテム購入用UIを起動します。
      購入処理はPlay Storeアプリにて行われるので、 アプリは"startIntentSenderForResult()"メソッドでこのインテントを起動し、 購入処理結果を、"onActivityResult()"で受け取る必要があります。
    • "onActivityResult()"へ返される情報には、購入された/キャンセル、 エラーが発生したを示すレスポンスコード、購入詳細情報、 開発者の秘密鍵で署名された購買データの署名が含まれます。
消費可能アプリ内管理アイテムの消費処理フロー図
消費可能アイテムの所持数(購入された数)は、Google Play にて管理しています。
アイテムの使用(プロビジョニング)する前に、Google Playに消費リクエストを送信しアイテムが正常に消費されたことを確認する必要があります。
この手順は、消費リクエストを送信する前にアプリが停止してたり、ネットワーク接続待ちをしている可能性があるため事前に必要です。

...とりあえずここまでが概要です。次はSubscriptionについて書きたいと思います。

2013年5月31日金曜日

Amazon In-App Purchasing API (アプリ内課金) テスト方法

Amazon Distribution Portalの公式マニュアル(英語)は下記URL先です。
(https://developer.amazon.com/sdk/in-app-purchasing/documentation/testing-iap.html)
開発環境と動かしたいアプリが揃ったら、次は動かす環境の整備です。

Amazon SDK APIのテストについて

 Amazon In-App Purchasing API には、検証環境というものが存在しません。しかも、Amazon SDK APIを通じてAmazon Client経由で通信を行う為には、Distribution Portalを通じて公開されているアプリでなければなりません。

SDK Tester

 それじゃ公開するまでテストもできないの?そんな訳ありません。Amazonはテストの為のAmazon Clientの動きをエミュレートするアプリを用意しています。

"SDK Tester" は、Amazon SDK の開発者がアプリ公開前に本番環境を想定してテストすることができる開発ツールです。これにより開発者は、Amazon SDK APIによって生成される全てのレスポンスをカバーするテストをアプリ公開前に実行できます。Amazonは、このSDK Testerでテストして動作したコードは本番でも同じく動作することを保証しています。

 SDK Testerでは、テストユーザーの購入情報(レシート)を消したり、キャンセル処理を行うことができます。また、ユーザーを変更したり、IAP-APIのレスポンスステータスを任意に変更することができます。

SDK Tester の仕組み

 IAP-APIは、動作しているアプリがDistribution Portalで公開されているアプリかどうかを判別し、公開されていない(開発中の)アプリである場合、SDKTesterアプリへ接続します。(もちろんSDK Testerをインストールしていないとだめですが。。。)

 公開後は勝手にIAP-APIがAmazonClient側へ接続しに行ってくれるので、テスト時と本番時に設定を変えるとかの差異がなく非常に便利です。

SDK Tester のインストール

 Amazon Distribution Portal (https://developer.amazon.com/sdk.html) からダウンロードできる、Amazon SDK (Apps-SDK.zip)に含まれています。

格納されている場所は、
"<Apps-SDK.zipを解凍したディレクトリ>/Apps-SDK/InAppPurchasing/tools/AmazonSDKTester.apk"です。

この AmazonSDKTester.apk を デバイスにインストールします。
※インストールには、Android 2.3.3 (Android API Level 10) 以上の端末が必要です。
 Kindleなら第1世代からインストールできます。

Windowsならコマンドプロンプトから以下のコマンドでインストールできます。
※<Android SDKのインストールされているディレクトリ>\sdk\platform-toolsへパスを通しておいてください。

C:\>adb install -s <デバイスID> <App-SDK.zipを解凍したディレクトリ>\Apps-SDK\InAppPurchasing\tools\AmazonSDKTester.apk
デバイスIDの調べ方は、"adb devices" コマンドで接続されているデバイス、エミュレータのデバイスIDを確認することができます。

アイテムデータ定義ファイルの準備

 Distribution Portalでも課金アイテムは定義しなければなりませんが、同じくSDK Testerにも課金アイテムの定義が必要です。
SDK Testerでは、JSONファイルで課金アイテムの定義を行います。

 SDK Testerは、デバイスの "/mmt/sdcard/amazon.sdktester.json"ファイルを読み込み、その情報をもとにIAP-APIへレスポンスしてくれます。 このときJSONファイルに定義する課金アイテムの情報へ本番と同じデータを入力できるのがいいところです。

  • 定義構造

     amazon.sdktester.json の定義はJSON形式に従い、以下のような構造で行います。この時、SKUの順序は関係ありません。
    {
        SKU_1:{IAP_Item_1_Description},
        SKU_2:{IAP_Item_2_Description},
        .
        .
        .
        SKU_n:{IAP_Item_n_Description
    }
            
    上記のとおり、複数のSKUを並べて定義できます。この時、SKU定義間の "," に注意してください。尚、コンテンツタイプによって項目の定義内容は異なります。

  • 定義要素:Consumable / Entitled

     Consumable Content(消費型コンテンツ)と Entitled Content(買いきり型コンテンツ)の課金アイテム定義項目は以下のとおりです。
    {
      ...
      ”(SKU文字列)” : 
      {
        "itemType": "ENTITLED" または "CONSUMABLE", 
        "price": (金額の数字),
        "title": "(商品名称)"
        "description": "(商品説明文)",
        "smallIconUrl": "(商品画像アイコン(サムネイル)URL)"
      }
      ...
    }
            

  • 定義要素:Subscription

     期間購入(Subscription)型コンテンツのアイテム定義項目は以下のとおりです。
    {
      ...
      ”com.amazon.buttonclicker.subscription.1mo (SKU文字列)” : 
      {
        "itemType": "SUBSCRIPTION",
        "price": (金額の数字),
        "title": "(商品名称)",
        "description": "(商品説明文)",
        "smallIconUrl": "(商品画像アイコン(サムネイル)URL)",
        "subscriptionParent": "com.amazon.buttonclicker.subscription"
      }
       ...
    }
            
     定義自体はほとんど変わりません。しかし、他と大きく違うのが "subscriptionParent"(親SKU)を指定している点です。 サブスクリプションの親SKUは、サブスクリプションが何のためにあるのかを表します。そしてその親SKUに紐づく子SKUは、価格や期間を表す条件のSKUとなります。
     この紐付けを行うことで、例えば「○○新聞の定期購読」の「1か月間のサブスクリプション」を購入したのに、 「1週間のサブスクリプション」を重複して買うことを防ぐことができます。

SDK Testerの使い方

 SDKテスターのスタート画面です。真ん中の3つのボタンをタップすると、それぞれの機能の画面を表示します。


  • Active Transactions

     有効な取引情報を表示します。また、テストの為にその取引をキャンセル(返品)すること取引情報をクリアすることができます。

  • Change Logged in User

     現在ログイン中のユーザー名を表示します。またログイン中のユーザー名を変更することができます。

  • Interactive Mode Preferences

     IAP-APIからのレスポンスを任意に設定できます。



    1. Purchase API

      PurchasingManager.initiatePurchaseRequest() -> ObserverのonPurchaseResponse()へのレスポンスの設定。
      • Default:正常
      • Already Entitled:既に購入済み
      • Invalid Sku:SKUが不正
      • Failed:リクエスト失敗
    2. Purchase Updates API

      PurchasingManager.initiatePurchaseUpdatesRequest() -> ObserverのPurchaseUpdatesResponse()へのレスポンスの設定。
      • Default:正常
      • Failed:リクエスト失敗
    3. Item Data API

      PurchasingManager.initiateItemDataRequest() -> ObserverのonItemDataResponse()へのレスポンスの設定。
      • Default:正常
      • Failed:リクエスト失敗


 これでアプリ内課金処理周りのテストケース前パターンをサクサクこなすことができます。

2013年5月29日水曜日

Amazon App Store In-App Purchasing API (アプリ内課金) の実装 #2

Amazon App Store In-App Purchasing API (アプリ内課金) の実装 #1 のつづき。
#1にて BasePurchasingObserver を継承したサブクラスまで記載したので、今度は PurchasingManager を使って実際に課金を行う処理について。

Purchasing Manager

  • In-App Purchasing API と情報をやりとりする重要なクラスです。Amazon Clientおよび Amazon Platform との間のアクセスを容易にします。
  • Purchasing Managerを使用するには、 #1で示したBasePurchasingObserverクラスを継承したサブクラスをPurchasing Managerへ登録する必要があります。アプリのActivityの onStart()メソッドで登録を行うのがベターです。

PurchasingManagerには以下の5つのメソッドがあります。
  1. registerObserver(PurchasingObserver purchasingObserver)
    • BasePurchasingObserver クラスを継承したサブクラスを PurchasingManager へ登録します。
    • 通常、Activity の onStart() メソッドにて呼出します。
  2. initiateGetUserIdRequest()
    • Amazon Client に現在ログインしているユーザーのアプリ特有IDの取得リクエストを送信します。
    • 通常、Activity の onStart() メソッドにて呼出しますが、いつでも取得できます。
  3. initiatePurchaseUpdatesRequest(Offset offset)
    • オフセットレシート情報のページ区分)指定されたレシート情報を取得するリクエストを送信します。
    • ここで返されるレシート情報は、Entitled Content(買い切り型コンテンツ)及び Subscription Content(期間購入型コンテンツ)のみで、Consumable Content(消費型コンテンツ)情報は取得できません。
    • デバイス間でユーザー購入情報を同期する処理に使用できます。
  4. initiateItemDataRequest(Set skus)
    • 指定されたSKU一連のアイテムデータを取得するリクエストを送信します。
    • コンテンツタイプ全てのアイテム情報を取得できます。
  5. initiatePurchaseRequest(String sku)
    • 指定されたSKUの購入リクエストを送信します。
      ※購入リクエストを行った後 Amazon Clientによって表示される購入ダイアログで購入ボタンをタップしない限り購入されません。
    • コンテンツタイプ全てのアイテムを購入できます。

PurchasingManagerを使う準備

 各所で記述していますが、PurchasingManagerを使用してアプリ内課金関連処理を行う前に最初にPurchasingManagerに対してコールバックさせる為のObserverを登録する必要があります。
@Override
protected void onStart() {
.
.
.
    // BasePurchasingObserverを継承して作成したサブクラスのインスタンスを取得
    IapObserver iapObserver = new IapObserver (this);

    // PurchasingManagerへObserverを登録する。
    com.amazon.inapp.purchasing.PurchasingManager.registerObserver(iapObserver);
.
.
.
}

アプリ特有IDの取得

 IAP-APIの課金では、Amazon ClientにログインしているユーザーIDに対して、そのアプリで使用する課金用のユーザーIDを使って管理を行います。

処理の手順は...
  1. PurchasingManager.initiateGetUserIdRequest()でユーザーID取得リクエストを行う。
  2. ObserverのonGetUserIdResponse(GetUserIdResponse getUserIdResponse)メソッドへコールバックする。
  3. 非同期処理でレスポンスオブジェクトからリクエストのステータス、ユーザーIDを取得してアプリで永続化する。

以下に、PurchasingManagerのユーザー取得リクエストメソッドと該当するObserverのメソッドを記述します。
Observerのより具体的な処理サンプルは #1のObserverサンプルコードを参照してください。
@Override
protected void onStart() {
.
.
.
// ユーザーID取得リクエストを行う
com.amazon.inapp.purchasing.PurchasingManager.initiateGetUserIdRequest();
.
.
.
}

@Override
public void onGetUserIdResponse(GetUserIdResponse getUserIdResponse) {
    // 非同期処理でレスポンスオブジェクトを検証
    new GetUserIdAsyncTask().execute(getUserIdResponse);
}

private class GetUserIdAsyncTask extends AsyncTask
{
    // onGetUserIdResponseで取得したGetUserIdResponseを格納
    GetUserIdResponse getUserIdResponse = params[0];
    
    if (getUserIdResponse.getUserIdRequestStatus() == GetUserIdRequestStatus.SUCCESSFUL) {
        // リクエスト成功。レスポンスからユーザーID取得
        Log.d(TAG, "UserID:" + getUserIdResponse.getUserId());
        // 永続化処理
        .
        .
        .
    } else {
        // リクエスト失敗
        .
        .
        .
    }
}

アイテムの購入処理

 やっと本題のアプリ内課金の購入処理についてです。実際の購入よる課金等の処理は全てIAP-APIが行ってくれるので、アプリ側で実装する内容は、購入情報(レシート)を検証して、その課金アイテムやコンテンツ・機能を使えるようにしてあげるというというものです。
 ストアに登録する商用アプリの場合は上記の他にも、購入権利をローカルに保持して不正が無い様定期的に権限を確認させる機能やマルチユーザー対応、消費型アイテム・コンテンツの管理、デバイス間同期処理を呼ぶ...などなど様々な前後処理が必要になります。しかし、ここではシンプルに購入 → 購入成功ダイアログ表示 までの処理を説明していきます。

※尚、Amazon Developer Portalでの課金アイテムの作り方や、SDKTesterの使い方・登録するJsonファイルなどについては触れません。この辺は別の記事に書いていこうと思います。

処理の手順は...
  1. PurchasingManager.initiatePurchaseRequest("購入アイテムのSKU")でアイテム購入リクエストを行う。
  2. Amazon Clientにて購入ダイアログが表示され、ユーザーが購入ボタンをタップするとIAP-APIで課金処理が行われる。
  3. ObserverのonPurchaseResponse(PurchaseResponse purchaseResponse)メソッドへコールバックする。
  4. 非同期処理でレスポンスオブジェクトからリクエストのステータス、コンテンツタイプなどのレシート情報を検証して課金コンテンツ・機能を利用可能にする。

以下に、PurchasingManagerの購入リクエストメソッドと該当するObserverのメソッドを記述します。
Observerのより具体的な処理サンプルは #1のObserverサンプルコードを参照してください。
// アイテムの購入リクエストを送信する。このリクエスト時にはリクエストIDが返り、
// コールバック後のレスポンスオブジェクトに含まれるリクエストIDと突き合わすことにより
// どのリクエストのレスポンスなのか判断する事ができる。
String requestId = com.amazon.inapp.purchasing.PurchasingManager.initiatePurchaseRequest("購入アイテムのSKU");

@Override
public void onPurchaseResponse(PurchaseResponse purchaseResponse) {
    new PurchaseAsyncTask().execute(purchaseResponse);
}

private class PurchaseAsyncTask extends AsyncTask {
    @Override
    protected Boolean doInBackground(PurchaseResponse... params) {
        // レシート検証処理
        .
        .
        .
    }
}

 非同期処理とUIスレッドの同期については、アプリによって色々な方法があると思いますので細かく触れませんが、私はCountDownLatchで同期させたりしてました。

上記に示した通り、IAP-APIを使うのはかなり簡単です。
PlayStoreの課金処理 (IAB-API v2とか特に)を実装した人ならIAP-APIの方が楽だと思います。

2013年5月28日火曜日

Amazon App Store In-App Purchasing API (アプリ内課金) の実装 #1

AmazonSDKのIn-App Purchasingを使った実装についてメモ。

アプリ内課金の購入処理概略

 In-App Purchasing (IAP) API を使ったアプリ内課金で購入するまでの大まかな処理フローを下図にまとめました。


(※詳細な処理については省略して書いています。)

大雑把に表現するとこんな手順・処理を踏んでアプリ内でアイテムの購入処理を行います。
ここで登場するのは以下の5つです。
  • ユーザー
    アプリを使用し、課金を行う人。
  • Activity
    課金を行うアプリのActivity(画面)。ここではUIスレッドで動作しているActivity。
  • Observer
    PurchasingManagerのレスポンスを監視し、そのレスポンスに対して処理を行うクラス。
  • PurchasingManager
    In-App Purchasing APIと購入情報等のやりとりを行うクラス。
  • AmazonClient
    Amazon App Store (Client)アプリ。ユーザーID管理、Amazon Platformとの通信を行い実際の課金処理を行うためのアプリ。
    実際はIAP-APIとしてPurchasingManagerを経由して情報を受渡されていますが、ここではわかりやすい実体をもつアプリで表記します。

フローを簡単に説明しますと、、、
  1. ユーザーがアプリの画面や確認ダイアログから、アイテム購入ダイアログを表示させる。
  2. ユーザーへAmazonClientのアイテム購入ダイアログを表示させるために、PurchasingManagerにて購入リクエストを送信する。
  3. 購入リクエストをAmazonClientに送信。
  4. 購入リクエストに対して対応するアイテムの購入ダイアログがユーザーに表示され、ユーザーが購入ボタンをタップする。
  5. AmazonClientにて購入処理が行われ、その購入処理結果をObserverにてキャッチする。
  6. レシート情報が送られてくるので、レシート情報をチェック。
  7. レシート情報に従い、購入された課金コンテンツ・機能を利用可能にする。
  8. 購入処理が正常に完了した通知を受け、AmazonClientが購入完了ダイアログをユーザーへ表示して一連の処理が完了する。
主要クラス

IAP-APIの処理にて、キーとなるクラスは以下の2つです。
  • com.amazon.inapp.purchasing.PurchasingManager
    Amazon Platformへのリクエストを行うためのクラスであり、 Amazon Platformからのレスポンスを取得し下記のオブサーバへ返すクラス。
    (フロー図の "PurchasingManager"です。)
  • com.amazon.inapp.purchasing.BasePurchasingObserver
    アプリにてAmazonPlatformからのレスポンスを取得する為に、PurchasingManagerへ登録し レスポンスを監視するクラス(抽象クラス)。
    (フロー図の "Observer" はこのクラスのサブクラスです。)

また、アプリ内課金で使用するクラス、用語について以下に簡単な説明を記述します。
  • アイテム:Item (com.amazon.inapp.purchasing.Item)
    • 課金アイテムのことを表します。商品タイトル、価格、商品説明などの商品情報を含みます。
    • 価格には通貨文字が含まれ、ロケールにもとづいて適切にフォーマットされています。
  • SKU (文字列)
    • 課金アイテムを表すユニークなID。
    • Amazon Developer Portalでログインし、アプリ情報のなかの [In-App Items] メニューにて作成できます。
  • レシート:Receipt (com.amazon.inapp.purchasing.Receipt)
    • ユーザーの全ての購入情報をもつオブジェクトです。
    • 全てのレシートは、レシート検証サービスを経由してアイテムの購入を検証する為に使用できます。 このレシート情報をもとにアプリ内でコンテンツや機能へのアクセス承認を行います。
  • オフセット:Offset (com.amazon.inapp.purchasing.Offset)
    • ページに分けられたレシート情報の位置を表します。
    • オフセットの値は、Base64エンコーディングされておりそのまま見て読める形式ではありません。

ResponceReceiverの登録

 IAP-APIは、全て非同期で処理が実行されます。アプリは、ResponceReceiverクラスを経由してAmazonClientからのBroadcast Intentを受信する必要があります。
 このResponceReceiverクラスは、アプリから直接使用する必要はありません。代わりにアプリに送られてくるBroadcast Intentを受信できる定義をアプリのAndroidManifest.xmlへ定義する必要があります。以下の通り記述してください。

.
.
.

    
        
        
    

.
.
.

※表示上の都合で、<action>タグの閉じるタグを別に設けてますが、「<action... />」で構いません。

BasePurchasingObserver

 Amazon Clientからの Broadcast Intent を取得する Response Receiverの登録が終わったら、次はResponse Receiverからのコールバックを受け取るクラスが必要になります。その為に、BasePurchasingObserverクラスを継承したサブクラスを作成しPurchasingObserverインタフェースを実装してPurchasingManagerへ登録する必要があります。

※このコールバック処理はUIスレッドで実行すると長時間UIスレッドを占有してしまうので通常はAsyncTaskなどの別スレッドで処理します。

以下は、BasePurchasingObserverを継承したサブクラスのサンプルコードです。
こんな感じのクラスを作って、ここでPurchasingManagerを経由してAmazonClientにリクエストした結果を取得し処理を行います。
package com.exsample.account;

import java.util.Date;
import java.util.LinkedList;
import java.util.Map;

import android.app.Activity;
import android.os.AsyncTask;
import android.util.Log;

import com.amazon.inapp.purchasing.BasePurchasingObserver;
import com.amazon.inapp.purchasing.GetUserIdResponse;
import com.amazon.inapp.purchasing.GetUserIdResponse.GetUserIdRequestStatus;
import com.amazon.inapp.purchasing.Item;
import com.amazon.inapp.purchasing.ItemDataResponse;
import com.amazon.inapp.purchasing.Offset;
import com.amazon.inapp.purchasing.PurchaseResponse;
import com.amazon.inapp.purchasing.PurchaseUpdatesResponse;
import com.amazon.inapp.purchasing.PurchasingManager;
import com.amazon.inapp.purchasing.Receipt;
import com.amazon.inapp.purchasing.SubscriptionPeriod;

public class SamplePurchasingObserver extends BasePurchasingObserver {
    
    // ログ出力用タグ
    private final static String TAG = "SamplePurchasingObserver";
    
    // コンストラクタでインスタンス化したアクティビティを受け取り
    // BasePurchasingObserverへコンテキスト渡す.
    public SamplePurchasingObserver(final Activity activity) {
        super(activity);
    }

    //
    // ObserverがPuchasingManagerに登録された時にコールバックします。
    // (PurchasingManager.registerObserver(observer)の後)
    // 
    // @param isSandboxMode サンドボックス環境で実行されているか否か
    //   true  : Amazon Client からの応答を受信している。
    //           (Amazon Client からダウンロードされたapkである場合はこちらを返す。)
    //   false : SDK Testerからの応答を受信している。
    //           (デバッグ、テスト用apkで動かしている場合はこちらを返す。)
    //
    @Override
    public void onSdkAvailable(boolean isSandboxMode) {
        PurchasingManager.initiateGetUserIdRequest();
    }

    // 
    // ユーザーID取得要求(PurchasingManager.initiateGetUserIdRequest())
    // のレスポンスが返った時にコールバックします。
    //
    // @param getUserIdResponse ユーザーID取得要求レスポンス
    //   リクエストが成功している場合、リクエストID、リクエストステータス及び、ユーザIDを含んだ状態で返ります。
    //
    @Override
    public void onGetUserIdResponse(GetUserIdResponse getUserIdResponse) {
        new GetUserIdAsyncTask().execute(getUserIdResponse);
    }

    //
    // アイテムデータ取得要求(PurchasingManager.initiateItemDataRequest())
    // のレスポンスが返った時にコールバックします。
    //
    // @param itemDataResponse アイテムデータ取得要求レスポンス
    //   リクエストが成功している場合、購入可能/購入不可能なアイテムのセットを含んだ状態で返ります。
    //
    @Override
    public void onItemDataResponse(ItemDataResponse itemDataResponse) {
        new ItemDataAsyncTask().execute(itemDataResponse);
    }

    //
    // 購入要求(PurchasingManager.initiatePurchaseRequest())
    // のレスポンスが返った時にコールバックします。
    //
    // @param purchaseResponse 領収書を含む購入要求レスポンス
    //   リクエストが成功している場合、リクエストID、リクエストステータス、購入した領収書を含んだ状態で返ります。
    //
    @Override
    public void onPurchaseResponse(PurchaseResponse purchaseResponse) {
        new PurchaseAsyncTask().execute(purchaseResponse);
    }

    //
    // 購入情報更新要求(PurchasingManager.initiatePurchaseUpdatesRequest())
    // のレスポンスが返った時にコールバックします。
    // この呼び出しは、ユーザーが別のデバイスにアプリをダウンロードした場合や、
    // アプリケーションを初期化・削除後再インストール後などに購入情報を同期する為に使用されます。
    //
    // @param purchaseUpdatesResponse
    //   リクエストが成功している場合、
    //   リクエストID、リクエストステータス、過去購入した領収書、取り消されたSKUのセット、及び該当する場合は次のオフセットを含んだ状態で返ります。
    //
    @Override
    public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse purchaseUpdatesResponse) {
        new PurchaseUpdatesAsyncTask().execute(purchaseUpdatesResponse);
    }
    
    // ユーザー情報取得レスポンスの実処理を実行するAsyncTask.
    // onGetUserIdResponse()へコールバックした後に実行。
    private class GetUserIdAsyncTask extends AsyncTask&ltGetUserIdResponse, Void, Boolean> {
        @Override
        protected Boolean doInBackground(GetUserIdResponse... params) {
            GetUserIdResponse getUserIdResponse = params[0];

            if (getUserIdResponse.getUserIdRequestStatus() == GetUserIdRequestStatus.SUCCESSFUL) {
                // ユーザーID取得ステータス成功時
                return true;
            } else {
                // ユーザーID取得ステータス失敗時
                Log.d(TAG, "onGetUserIdResponse: Unable to get user ID.");
                return false;
            }
        }
        @Override
        protected void onPostExecute(Boolean result) {
            super.onPostExecute(result);
            if (result) {
                // ユーザーID取得成功時...購入状態(購入/取消)を更新する要求を開始するなどの処理を記述
            } else {
                // ユーザーID取得失敗時...エラーダイアログ表示処理など
            }
        }
    }
    
    //
    // 課金アイテム情報取得レスポンスの実処理を実行するAsyncTask
    // onItemDataResponse()へコールバックした後に実行。
    // ゲームのストアフロントでアプリ内課金アイテムを表示するときにこの情報を使用します。
    //
    private class ItemDataAsyncTask extends AsyncTask&ltItemDataResponse, Void, Void> {
        @Override
        protected Void doInBackground(ItemDataResponse... params) {
            final ItemDataResponse itemDataResponse = params[0];
            
            switch (itemDataResponse.getItemDataRequestStatus()) {
            case SUCCESSFUL_WITH_UNAVAILABLE_SKUS:
                // 購入できないアイテムのSKU
                for (final String sku : itemDataResponse.getUnavailableSkus()) {
                    Log.d(TAG, "Unavailable SKU:" + sku);
                }
            case SUCCESSFUL:
                // 購入可能なアイテムのSKU
                final Map&ltString, Item> items = itemDataResponse.getItemData();
                for (final String key : items.keySet()) {
                    Item i = items.get(key);
                    Log.d(TAG, "Item: " + i.getTitle() + "\n " +
                               "Type: " + i.getItemType() + "\n " +
                               "SKU: " + i.getSku() + "\n " +
                               "Price: " + i.getPrice() + "\n " +
                               "Description: " + i.getDescription() + "\n");
                }
                break;
            case FAILED:
                // レスポンス失敗
                break;
            }
            return null;
        }
    }

    //
    // onPurchaseResponse()を受信した後に呼び出されます。
    // オブザーバが購入の応答を受信したときに開始され、AsyncTaskが正常に返されるとUIが更新されます。
    //
    private class PurchaseAsyncTask extends AsyncTask&ltPurchaseResponse, Void, Boolean> {
        @Override
        protected Boolean doInBackground(PurchaseResponse... params) {
            final PurchaseResponse purchaseResponse = params[0];
            if (!purchaseResponse.getUserId().equals("[購入リクエストを行ったユーザーID]")) {
                // カレントログインユーザIDがPurchaseレスポンスから返されたユーザIDと違う場合、
                // レスポンスに含まれるユーザーIDの購入情報を更新する。 
                // ※通常はユーザー別にオフセットの値をこどかへ永続化させておき、その値で購入情報を取得する。
                PurchasingManager.initiatePurchaseUpdatesRequest(Offset.fromString("[リクエストに含まれるユーザーのオフセット値]"));
            }

            switch (purchaseResponse.getPurchaseRequestStatus()) {
            case SUCCESSFUL:
                // 購入成功:
                // レシート情報から購入されたアイテムタイプ・レシート情報を検証し、課金アイテムを利用可能にします。
                final Receipt receipt = purchaseResponse.getReceipt();
                switch (receipt.getItemType())
                {
                case CONSUMABLE:
                    // 消費型コンテンツの場合の処理を記述
                    Log.d(TAG, "Purchasing Response Item is CONSUMABLE.\n" + "Sku=" + receipt.getSku());
                    break;
                case ENTITLED:
                    // 買い切り型コンテンツの場合の処理を記述
                    Log.d(TAG, "Purchasing Response Item is ENTITLED.\n" + "Sku=" + receipt.getSku());
                    break;
                case SUBSCRIPTION:
                    // 期間購入型コンテンツの場合の処理を記述
                    Log.d(TAG, "Purchasing Response Item is Subscription.\n" + "Sku=" + receipt.getSku());
                    break;
                default:
                    break;
                }
                return true;
            case ALREADY_ENTITLED:
                // 重複購入:
                // 顧客が既にアイテムを受け取っている場合、レシート情報は戻りません。
                // レスポンスに格納されたリクエストIDと、PurchasingManagerで購入リクエストを送信するときに返されるリクエストIDを照合することによって、
                // 購入処理が重複したのかを判断し、必要な処理を行います。
                Log.d(TAG, "レスポンスに格納されたリクエストID=" + purchaseResponse.getRequestId());
                break;
            case FAILED:
                // 購入失敗:
                // SKUの購入が無効・購入しなかったなどの場合は、FAILEDステータスが返ります。
                // アプリケーションからリクエストされたSKUとDevPortalのSKUに相違がある場合に発生することがあります。
                break;
            default:
                break;
            }
            return false;
        }

        @Override
        protected void onPostExecute(Boolean success) {
            super.onPostExecute(success);
            if (success) {
                // 購入成功/重複購入時
            } else {
                // 購入:キャンセルor失敗時
            }
        }
    }

    //
    // Started when the observer receives a Purchase Updates Response Once the AsyncTask returns successfully, we'll
    // update the UI.
    // 固有ユーザIDに紐づく更新された購入情報を処理...
    // オブサーバがonPurchaseUpdatesResponseを受信したときに開始されUI(画面オブジェクト)を更新する。
    //
    private class PurchaseUpdatesAsyncTask extends AsyncTask&ltPurchaseUpdatesResponse, Void, Boolean> {
        @Override
        protected Boolean doInBackground(PurchaseUpdatesResponse... params) {
            final PurchaseUpdatesResponse purchaseUpdatesResponse = params[0];

            // 更新購入情報のユーザIDが一致する事を確認
            if (!(purchaseUpdatesResponse.getUserId().equals("[購入リクエストを行ったユーザーID]"))) {
                return false;
            }

            // 何らかの理由で顧客が商品を取り消された場合、これらのアイテムのSKUは失効SKUのセットに含まれます。
            // 取り消されたSKUを取得して取り消された購入情報を更新します。
            // ※注意:
            //  getRevokedSkus()は、Entitlement Content (買い切り型コンテンツ) のみに適用され、
            //  期間購入型コンテンツ(Subscription Content)には適用されない。
            for (final String sku : purchaseUpdatesResponse.getRevokedSkus()) {
                Log.d(TAG, "Revoked Sku:" + sku);
            }
            
            switch (purchaseUpdatesResponse.getPurchaseUpdatesRequestStatus()) {
            case SUCCESSFUL:
                // 最終サブスクリプション期間保持変数
                SubscriptionPeriod latestSubscriptionPeriod = null;

                // サブスクリプション格納コレクション
                final LinkedList&ltSubscriptionPeriod> currentSubscriptionPeriods = new LinkedList&ltSubscriptionPeriod>();

                // 更新購入情報の全レシートをを読んでサブスクリプションの期間を取得
                for (final Receipt receipt : purchaseUpdatesResponse.getReceipts()) {
                    // レシートのアイテムタイプを取得してアイテムタイプ別に処理を行う
                    switch (receipt.getItemType()) {
                    case ENTITLED:
                        // 売り切り型コンテンツのレシートだった場合
                        Log.d(TAG, "receipt item type is ENTITLED.");
                        break;
                    case SUBSCRIPTION:
                        // 期間購入型(サブスクリプション)コンテンツのレシートだった場合
                        // サブスクリプションの購入情報更新は、次のいずれかの方法で行うことができます:
                        // 1. レシート情報から、ユーザーが現在有効なサブスクリプションを持っているかどうかを判断する。
                        //    ...有効なサブスクリプションを探すため、レシートから終了日のないサブスクリプションがあるかどうかをチェックする。
                        // 2. レシート情報から、ユーザーのサブスクリプション購入履歴を作成する。
                        //    ...過去の有効なサブスクリプションに基づいてコンテンツをアンロックするアプリでは、顧客の購買履歴を作成する必要があります。
                        //       例) 購入者が雑誌の年間購読サブスクリプションを持っている場合、
                        //           例え顧客が現在有効なサブスクリプションを持っていなくとも、顧客は購入したときから現在も雑誌へのアクセス権を持っていることになります。
                        Log.d(TAG, "Receipt Item Type is Subscription.");
                        
                        if (receipt.getSku().equals("アプリで購入できる商品のSKU")) {
                            // サブスクリプションの期間
                            final SubscriptionPeriod subscriptionPeriod = receipt.getSubscriptionPeriod();
                            // サブスクリプション開始日時
                            final Date startDate = subscriptionPeriod.getStartDate();

                            // 最新の開始日を持つ領収書を探す。
                            // 重複するサブスクリプションがある場合は、現在のサブスクリプションコレクションに追加
                            if (latestSubscriptionPeriod == null || startDate.after(latestSubscriptionPeriod.getStartDate())) {
                                // サブスクリプションの最終日時がNUll又は、
                                // 現在のサブスクリプションの開始日時より最後のサブスクリプション期間の開始日が後の場合
                                currentSubscriptionPeriods.clear();
                                latestSubscriptionPeriod = subscriptionPeriod;
                                currentSubscriptionPeriods.add(latestSubscriptionPeriod);
                            } else if (startDate.equals(latestSubscriptionPeriod.getStartDate())) {
                                // サブスクリプションの最終日時がNullでない又は
                                // 現在のサブスクリプションの開始日時より最後のサブスクリプション期間の開始日時が前の場合
                                currentSubscriptionPeriods.add(receipt.getSubscriptionPeriod());
                            }
                        }
                        break;
                    default:
                        break;
                    }
                }

                // すべての領収書が読まれた後、最後(最新)のサブスクリプションを確認します。
                // サブスクリプション終了日が入っている場合、サブスクリプションは期限切れ(無効)です。
                if (latestSubscriptionPeriod != null) {
                    // 全サブスクリプションのコレクションからサブスクリプション期間を取得
                    for (SubscriptionPeriod subscriptionPeriod : currentSubscriptionPeriods) {
                        if (subscriptionPeriod.getEndDate() != null) {
                            // サブスクリプション期間の終了日がある場合、
                            // そのサブスクリプションは有効ではないと判断し、課金アイテムをロックする処理を入れる。
                            break;
                        }
                    }
                }

                // レシートのオフセット永続化処理
                final Offset newOffset = purchaseUpdatesResponse.getOffset();
                // どこかへ保存...
                
                if (purchaseUpdatesResponse.isMore()) {
                    // レシート情報がまだあある場合
                    Log.d(TAG, "Initiating Another Purchase Updates with offset: " + newOffset.toString());

                    // 現在のオフセットを指定してさらに購入状態(購入/取消)情報を更新する要求を開始。
                    // onPurchaseUpdatesResponse(PurchaseUpdatesResponse) にコールバックします。
                    PurchasingManager.initiatePurchaseUpdatesRequest(newOffset);
                }
                return true;
            case FAILED:
                // 購入情報の取得に失敗した場合。
                return false;
            }
            return false;
        }

        @Override
        protected void onPostExecute(Boolean result)
        {
            super.onPostExecute(result);
            // レスポンスに含まれるリクエストIDや処理結果をもとに処理記述
        }
    }
}

長くなってきたので続きは #2 で。

2013年5月27日月曜日

Amazon App Store In-App Purchasing API (アプリ内課金) 説明

 以前、Amazon app storeにアプリ内課金(サブスクリプション)を実装したアプリを提供した。
その時、Amazon SDKにおける In-App Purchasing APIを使用してアプリ内課金処理を実装したのでその仕組みのメモ。

Amazon App Store

 PC、KindleやAndroidスマートフォン/タブレット向けのアプリマーケット。
(Amazon版 Play Store的なもの)
ユーザーはいつものAmazonアカウントで、いつものお買い物と同じようにアプリやコンテンツを購入することが可能。アプリ提供の他、おすすめ機能、カスタマーレビュー、1-Click決済を含むAmazon同等のショッピング機能が用意されている。
Androidデバイスでこのサービスを利用する為には、Amazon App Store(Client)アプリ(Googleでいうところの Play Storeアプリ的なもの)が必要。

課金の概要

 Amazon App Storeでは、アプリの中からゲーム内通貨、拡張パック、アップグレード、雑誌の購入などをできるようにする仕組みとして、IAP (In-App Purchasing) APIを公開しています。
(AmazonSDKとしてJARライブラリとして公開されており、ポータルサイトからダウンロードできます。)
IAP APIを利用するとデジタルコンテンツやサブスクリプション(定期購読などのこと)をアプリ内で販売することが可能になります。

課金の仕組み

  1. Amazon App Store (Client) と Amazon Service
     Kindle, Androidデバイスにおける Amazon App Storeの課金は、amazon app clientアプリが Amazonのサーバーへ3G/Wi-Fi回線を使用してリクエストを行い、領収書(レシート)を取得してアプリへ返すことで実現しています。

  2. Amazon Clientが担う機能
    • アプリ側で実装しなければならない機能
      1. ユーザーが購入できるようにアプリ内のアイテムのカタログを提示する。
      2. 購入されたアイテム・コンテンツのロック解除。
      3. 購入された消耗アイテムのトラッキング。
      4. 購入したデジタルコンテンツの使用。
    • Amazon App Store (Client)側で行われる機能
      1. 購入手順の管理。
      2. 支払処理の実施。
      3. 決済のセキュリティを含め、Amazon Platformとのセキュアな通信を行う。
      4. 資格を確認し、購入した領収書を検証する。
      5. サブスクリプションの自動更新や取り消しを含む管理を行う。
  3. コンテンツの販売方法
    1. ローカルコンテンツのロック解除方式
    2. リモートコンテンツのダウンロード方式
  4. SKU (technically a stock-keeping unit)
    • 購入可能なアイテムのユニーク(一意)識別子。
    • amazon Distribution Portalの開発者アカウント内で一意である必要がる。
    • ユーザが購入する直接のアイテム定義である。
購入可能なアイテム

Amazon App Storeで購入可能なアイテムは3種類。
  1. Consumable Content(消費型コンテンツ)
    使用に資格やアクセス権を必要とせず、何度でも購入可能。
    そのデバイスでのみ利用可能なコンテンツ。
  2. Entitled Content(買いきり型コンテンツ)
    使用する権利やアクセス権を必要とするコンテンツ。
    1度だけ購入しユーザアカウントが登録されている全ての互換性のあるデバイスで使用することができる。
  3. Subscription Content(期間購入型コンテンツ)
    一定の期間、資格やアクセス権を必要とするコンテンツ。 一定の期間その権利を持ち、自動で更新される。購入したユーザアカウントが登録されている全ての互換性のあるデバイスで使用することができる。
※名称等が若干違う場合があります。

処理のフローや実装、API(PurchasingManagerやBasePurchasingObserver)の詳細はまた別の記事で記載していきたいと思います。
記事内容に誤り等がありましたらご指摘願います。

2013年5月25日土曜日

Amazon AppStore アプリ内課金の実装--準備

はじめる前に

 In-App Purchasing API は、Amazon Mobile App SDKパッケージの一部として利用可能です。 これは、Androidアプリケーションプロジェクトに含めることができるJARライブラリとして提供されています。

 In-App PurchasingのJARライブラリは任意のIDEで利用可能ですが、ここではEclipseを使用して説明しています。 この作業を行う前に、Android開発環境が構築されている必要があります。また、この説明ではIn-App Purchasingを実装する対象となるAndroidアプリケーションプロジェクトが作成されていることを想定しています。作成されていない場合は作成してから作業を行ってください。

App-SDKをダウンロードして展開する

  1. [Apps-SDK.zip] ファイルを Amazon mobile app distribution Portal よりダウンロードしてください。 (https://developer.amazon.com/sdk/download/in-app-purchasing.html)
  2. ダウンロードした [Apps-SDK.zip] ファイルを解凍し、わりやすい場所へ置いてください。
    (※このディレクトリはプロジェクトから参照するようにするため後々移動しなくてよい場所へ配置してください。)

In-App Purchasing JARライブラリをクラスパスに追加する

  1. 追加するプロジェクトのルートフォルダを選択し、コンテキストメニューから [プロパティ] を選択します。
  2. プロパティウィンドウの左ツリーより、[Javaのビルドパス]を選択。
  3. 上部の[ライブラリ] タブをクリック。
  4. 右側の [外部 JAR 追加] ボタンをクリック。
  5. "JAR の選択"ダイアログで、/InAppPurchasing/lib/in-app-purchasing-1.0.3.jarを選択し [開く] ボタンをクリック。
  6. ビルドパスに "in-app-purchasing-1.0.3.jar" が追加されたことを確認して [OK] ボタンでプロパティウィンドウを閉じます。

In-App Purchasing APIリファレンスを追加する

  1. 追加するプロジェクトのルートフォルダを選択し、コンテキストメニューから [プロパティ] を選択します。
  2. プロパティウィンドウの左ツリーより、[Javaのビルドパス]を選択。
  3. 上部の[ライブラリ] タブをクリック。
  4. 先の手順で追加した、"in-app-purchasing-1.0.3.jar" の左側▲をクリックして詳細を表示させる。
  5. [Javadoc ロケーション: (なし)]をダブルクリックまたは、選択して右側の[編集]ボタンをクリック。
  6. "’in-app-purchasing-1.0.3.jar' の Javadoc" ウィンドウで、 Javadoc URLラジオボタンを選択し、Javadoc ロケーション・パスに "/InAppPurchasing/documentation/API-Reference" を指定する。
  7. [in-app-purchasing-1.0.3.jar] の Javadoc ロケーションが変更されたことを確認して、[プロパティ]ウィンドウを [OK]ボタンで閉じる。
これでプロジェクト内でアプリ内課金関連のパッケージのリファレンスがEclipse上で直接見れるようになります。