メインコンテンツまでスキップ

チュートリアルデスクトップとモバイルデバイスでのアプリ内課金

📚 Source Page

デスクトップアプリケーションとモバイルアプリケーションで、消耗品と非消耗品のアプリ内課金を販売できます。macOS/iOSとAndroidデバイスの両方で、IAP製品のセットアップと支払い処理方法をご紹介します。

レベル:上級

プラットフォーム:macOS, iOS, Android

クラス: InAppPurchases::Listener,SoundPlayer,AsyncUpdater,ListBoxModel

警告

このプロジェクトには、macOS/iOSではApple DeveloperアカウントとiTunes Connectアカウント、AndroidではGoogle Play Developerアカウントが必要です。ヘルプが必要な方はApple Developer,iTunes ConnectそしてGoogle Play Developerのウェブサイトで口座を開設する。

シミュレーターはIAPテストをサポートしていないため、このプロジェクトではアプリ内課金をテストするために物理的なデバイスが必要です。このためにデバイスを用意してください。

スタート

このチュートリアルのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP.プロジェクトを解凍し、最初のヘッダーファイルをProjucerで開く。

警告

このプロジェクトのPIPバージョンを使用する場合は、必ずResourcesフォルダを生成されたProjucerプロジェクトに追加します。

このステップでヘルプが必要な場合は、以下を参照してください。Tutorial: Projucer Part 1: Getting started with the Projucer.

デモ・プロジェクト

このプロジェクトでは、音声ディクテーションを使用してフレーズを再生する際に、いくつかの聴覚的なフレーバーを提供するために、アプリ内課金で取得できるさまざまな音声を提供しています。デフォルトでは、ユーザーは一般的なロボット音声を利用できますが、必然的にJUCE開発者の音声を試したくなります。iOSシミュレータでモバイル・アプリケーションを実行すると、ウィンドウは次のようになります:

iOSのデモ・プロジェクト・アプリケーションのウィンドウ
iOSのデモ・プロジェクト・アプリケーションのウィンドウ
注記

アプリケーションはシミュレータで実行されているので、商品がグレーアウトし、価格が無期限にフェッチされるのは予想された動作です。

警告

これはチュートリアル・プロジェクトですが、サンドボックス・モードかテスト・ユーザーとしてログインしていない限り、商品を購入しようとするとクレジットカードに課金されます!

注記

ここで紹介するコードは、大まかに以下のものと似ている。アプリ内購入JUCE Examplesより。

初期設定

このプロジェクトが適切に機能するためには、特定のデプロイメント・プラットフォームに適した開発者コンソールで、いくつかの初期設定手順を実行する必要があります。まず、Projucerでアプリ内課金の適切なパーミッションを許可しましょう。macOS/iOSではアプリ内課金機能のチェックボックスがオンになっていることを確認してください。Androidでアプリ内課金チェックボックスをオンにする。

iOSのプロジェクト設定ウィンドウ
iOSのプロジェクト設定ウィンドウ
Androidのプロジェクト設定ウィンドウ
Androidのプロジェクト設定ウィンドウ

プロジェクトを保存し、お気に入りのIDEで開くと、Projucerは自動的に必要なエンタイトルメントをデプロイメント・ターゲットに追加します。

アップル開発者

注記

アンドロイド用に開発する場合は、次のセクションに進んでください。Google Play Developerをご覧ください。

macOS と iOS では、アプリケーションに署名するために、Xcode 内の Apple Developer アカウントでサインインし、開発チームを選択する必要があります。あなたのプロジェクトのためのユニークなバンドル ID を選択します。以下のスクリーンショットのように、Xcode は自動的に署名証明書と Provisioning Profile を提供します:

Xcodeの一般設定ウィンドウ
Xcodeの一般設定ウィンドウ

また、正しいアプリの機能がチェックされ、承認されていることも確認してください。能力設定ウィンドウを開きます。以下のように同じ情報が表示されるはずです:

Xcodeの機能設定ウィンドウ
Xcodeの機能設定ウィンドウ

iTunes Connect

iOSでアプリ内課金を正しく表示するには、iTunes ConnectでIAP商品を作成する必要があります。まず、ダッシュボードの私のアプリ.に移動すると特徴タブにアクセスできます。アプリ内課金機能を以下の画面で見ることができる:

iTunes ConnectでIAP製品を作成する
iTunes ConnectでIAP製品を作成する

をクリックしてください。**+**適切な名前と価格で、アプリのオプションに対応する6つの製品を作成するためのサイン。

Google Playデベロッパー

注記

macOS/iOS用に開発する場合は、前のセクションにジャンプしてください。Apple Developerをご覧ください。

Androidでアプリ内課金を正しく表示するには、Google Play Consoleでアプリ内課金商品を作成する必要があります。まず、アプリのページをすべてのアプリケーションパネルを開きアプリ内商品ページ下店舗プレゼンス.の下にある項目にアクセスできます。マネージド製品タブをクリックする:

Google Play ConsoleでIABプロダクトを作成する
Google Play ConsoleでIABプロダクトを作成する

をクリックする。マネージド製品を作るアプリのオプションに対応する6つの商品を、適切な名前と価格で作成する。

注記

Google PlayのプロダクトIDに関する制限により、プロダクトIDには数字(0~9)、小文字(a~z)、アンダースコア(_)、またはフルストップ(.)のみを使用することをお勧めします。こうすることで、AppStoreとPlayStoreの両方で同じプロダクトIDを使用することができます。そうしないと、アプリは同じ商品でも異なるプロダクトIDを扱わなければならなくなります。

Android APKを生成する

Androidでアプリ内課金を正しく機能させるためには、Android版アプリに署名し、Google PlayストアAPIへのリクエストを認証する必要があります。まずAndroid Studioを起動し、メニューバーのビルド > 署名付きAPKの生成...:

メニューバーから署名付きAPKを生成する
メニューバーから署名付きAPKを生成する

すると、キーストア・ファイルの場所、エイリアス、パスワードを入力するようプロンプトが表示される:

キーストア・ファイルのエイリアスとパスワードを入力する。
キーストア・ファイルのエイリアスとパスワードを入力する。

次に、リリースビルドタイプの "release_"フレーバーを選択し、V1とV2の両方の署名がチェックされていることを確認する。

ビルド・タイプ、フレーバー、シグネチャー・バージョン
ビルド・タイプ、フレーバー、シグネチャー・バージョン

これでキーストア・ファイルが生成され、Projucerで参照できるようになる。Androidリリース設定で、キーストア・ファイルへの相対パス、エイリアス、パスワードを鍵の署名の分野だ:

リリース設定のキーストアファイルのパス
リリース設定のキーストアファイルのパス

アプリ内購入のセットアップはもう完了しているはずで、ようやくアプリにこれらの機能を実装し始めることができる。

購入タイプ

アプリ内課金は、アプリ内で直接顧客に追加コンテンツや機能を提供するのに便利です。プレミアム機能、限定アイテム、あるいはサブスクリプションなどがあります。一般的に、すべての関連プラットフォームで4つの主要なタイプの購入があります:

  • 消耗品:複数回使用・購入可能なアイテム。
  • 非消耗品:アプリの機能を永続的にアンロックする1回限りの購入。
  • 自動更新サブスクリプション:定期的に更新されるコンテンツを、解約するまで定期的にお届けします。
  • 更新されないサブスクリプション:手動更新が必要な期間限定コンテンツ。

このチュートリアルでは、非消費型のアプリ内課金を実装します。

プロジェクト体制

プロジェクトは、アプリケーションのさまざまな部分を処理するために、以下のクラスを使用して構成されています:

  • MainContentComponent:画面上にGUIコンポーネントをレイアウトし、再生/停止ボタンクリック時のサウンドファイルの再生を処理する。
  • フレーズモデル:ListBoxModel購入したボイスを使って演奏できるフレーズを説明する。
  • ボイスモデルListBoxModelをクリックすると、フレーズを再生するために購入可能なボイスが表示されます。
  • VoiceRow:カスタム行Componentの VoiceModel クラスを使用して、特定の音声エントリーの画像と情報を表示します。
  • VoicePurchases:VoiceProductの購入を管理するクラス。InAppPurchasesインスタンスだ。
  • VoiceProduct:価格、名前、以前に購入したかどうかなどの情報を持つ、単一の購入可能な製品を記述するための構造。

見てわかるように、これはおおよそMVCデザイン・パターンを使って、アプリケーションの機能を別々のクラスに分離する。

VoiceProduct 構造体には、消耗品以外の製品に役立つ情報、つまり以下の変数が含まれています:

  • const char* identifier:iTunes ConnectやGoogle Playで参照される一意の識別子。
  • const char* humanReadable:アプリに表示する、人間が読めるバージョンの識別子。
  • bool isPurchased:ログインしているユーザがそのアイテムを購入したことがあるかどうか。
  • bool priceIsKnown:価格が読み込まれ、表示されるように取得されているかどうか。
  • bool purchaseInProgress:ユーザーが現在進行中の購入を開始したかどうか。
  • String購入価格:商品価格を現地通貨で表示するローカライズされた文字列。
エクササイズ

消耗品の購入にはどのような情報が必要だろうか?定期購入についてはどうだろう?何が追加されるべきか、何が削除されるべきかを考えよう。

製品の表示

販売したいIAP商品をユーザー・インターフェースに表示することから始めましょう。VoicePurchasesコンストラクタのVoicePurchasesクラスでは、VoiceProductオブジェクトを上記のような構造で初期化しています:

    VoicePurchases()
{
voiceProducts = juce::Array(
{ VoiceProduct {"robot", "Robot", true, true, false, "Free" },
VoiceProduct {"jules", "Jules", false, false, false, "Retrieving price..." },
VoiceProduct {"fabian", "Fabian", false, false, false, "Retrieving price..." },
VoiceProduct {"ed", "Ed", false, false, false, "Retrieving price..." },
VoiceProduct {"lukasz", "Lukasz", false, false, false, "Retrieving price..." },
VoiceProduct {"jb", "JB", false, false, false, "Retrieving price..." } });
}
警告

これらのプロダクトIDは、iTunes ConnectやGoogle Play Consoleの値と正確に一致しなければ正しく動作しません。

すべての製品について、人間が読めるバージョンを簡単に取得できるように、私たちはヘルパー関数を実装しました。StringArrayすべて大文字の名前:

    juce::StringArray getVoiceNames() const
{
juce::StringArray names;

for (auto& voiceProduct : voiceProducts)
names.add (voiceProduct.humanReadable);

return names;
}

同じクラス内にもうひとつ、次のようなヘルパー関数を作ってみよう。findVoiceIndexFromIdentifier()を使用して、識別子を渡された VoiceProduct のインデックスを取得します。このプライベート関数は、後でInAppPurchasesオブジェクトがある:

    int findVoiceIndexFromIdentifier (juce::String identifier) const
{
identifier = identifier.toLowerCase();

for (auto i = 0; i < voiceProducts.size(); ++i)
if (juce::String (voiceProducts.getReference (i).identifier) == identifier)
return i;

return -1;
}

についてInAppPurchasesクラスはブロードキャスターとして機能する。InAppPurchases::Listenerこのクラスのリスナーになり、IAPサーバーからのコールバックを受け取る。[1]:

class VoicePurchases      : private juce::InAppPurchases::Listener // [1]
{
public:

これでInAppPurchasesコールバック関数を実装します。まずproductsInfoReturned()関数を呼び出します。この関数はInAppPurchases::getProductsInformation().

    void productsInfoReturned (const juce::Array& products) override
{
if (! juce::InAppPurchases::getInstance()->isInAppPurchasesSupported()) // [2]
{
for (auto idx = 1; idx < voiceProducts.size(); ++idx) // [3]
{
auto& voiceProduct = voiceProducts.getReference (idx);

voiceProduct.isPurchased = false;
voiceProduct.priceIsKnown = false;
voiceProduct.purchasePrice = "In-App purchases unavailable";
}

juce::AlertWindow::showMessageBoxAsync (juce::AlertWindow::WarningIcon, // [4]
"In-app purchase is unavailable!",
"In-App purchases are not available. This either means you are trying "
"to use IAP on a platform that does not support IAP or you haven't setup "
"your app correctly to work with IAP.",
"OK");
}
else
{
for (auto product : products)
{
auto idx = findVoiceIndexFromIdentifier (product.identifier); // [5]

if (juce::isPositiveAndBelow (idx, voiceProducts.size())) // [6]
{
auto& voiceProduct = voiceProducts.getReference (idx);

voiceProduct.priceIsKnown = true;
voiceProduct.purchasePrice = product.price;
}
}

juce::AlertWindow::showMessageBoxAsync (juce::AlertWindow::WarningIcon, // [7]
"Your credit card will be charged!",
"You are running the sample code for JUCE In-App purchases. "
"Although this is only sample code, it will still CHARGE YOUR CREDIT CARD!",
"Understood!");
}

guiUpdater.triggerAsyncUpdate();
}
  • [2]まず、アプリ内課金があなたのプラットフォームでサポートされているか、セットアップが正しく行われているかを確認します。
  • [3]: 最初に空いているVoiceProductオブジェクトを除くすべてのVoiceProductオブジェクトについて、商品が入手できないことを反映するように変数を設定する。
  • [4]オプションで非同期にメッセージボックスを表示し、ユーザーに問題を説明します。
  • [5]: アプリ内課金が可能な場合、すべての VoiceProduct オブジェクトについて、先に実装したヘルパー関数で識別子からインデックスを取得します。
  • [6]インデックスが有効であれば、商品変数に商品の在庫状況を反映させます。
  • [7]オプションで、非同期にユーザーに課金されることを説明するメッセージボックスを表示できます。

購入品の検索

警告

このセクションは物理的なデバイス上でのみ機能します。シミュレーターでは正しく機能しないので、試さないでください。

アプリ内課金を扱う場合、アプリケーションが最初にチェックすべきなのは、ユーザーがサインインしたときに過去の購入履歴があるかどうかです。顧客として最もイライラすることの1つは、別のデバイスや同じアプリの以前のバージョンで購入したものを失うことです。そこで、アプリ起動時にできるだけ早く、ユーザーが商品ページの最新ビューを取得できるようにしましょう。

まずVoicePurchasesアプリの一時的な状態を保存するクラス[1]:

    bool havePurchasesBeenRestored = false, havePricesBeenFetched = false, purchaseInProgress = false; // [1]
juce::Array voiceProducts;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoicePurchases)
};

という新しいヘルパー関数も必要だ。getPurchase()インデックスから VoiceProduct を取得します。この関数には、この関数が最初に呼ばれたときのための初期化コードも挿入します:

    VoiceProduct getPurchase (int voiceIndex)
{
if (! havePurchasesBeenRestored)
{
havePurchasesBeenRestored = true; // [2]
juce::InAppPurchases::getInstance()->addListener (this); // [3]

juce::InAppPurchases::getInstance()->restoreProductsBoughtList (true); // [4]
}

return voiceProducts[voiceIndex]; // [5]
}
  • [2]この関数が初めて呼び出される場合は、このコードセグメントが再び呼び出されないようにしてください。
  • [3]このクラスをInAppPurchasesインスタンスでコールバックを受け取ります。
  • [4]を呼び出すことができる。restoreProductsBoughtList()関数を使用して復元処理をトリガーし、その後のコールバックを待つ。
  • [5]最後に、指定されたインデックスのVoiceProductを返します。これは、この関数の通常の動作として、毎回呼び出されます。

VoicePurchasesクラスは、上記の関数が最初に呼ばれたときにリスナーとして登録されるので、このクラスが破棄されたときに登録を解除する必要があります。このクラスをInAppPurchasesリスナーをクラスのデストラクタに追加する:

    ~VoicePurchases() override
{
juce::InAppPurchases::getInstance()->removeListener (this);
}

先に定義したgetPurchase()関数を呼び出した。restoreProductsBoughtList()関数を使用してコールバックをトリガーし、購入した商品を復元します。では、そのコールバック関数を実装してみましょう。purchasesListRestored():

    void purchasesListRestored (const juce::Array& infos, bool success, const juce::String&) override
{
if (success)
{
for (auto& info : infos)
{
for (const auto& productId : info.purchase.productIds)
{
auto idx = findVoiceIndexFromIdentifier (productId); // [6]

if (juce::isPositiveAndBelow (idx, voiceProducts.size()))
{
auto& voiceProduct = voiceProducts.getReference (idx); // [7]

voiceProduct.isPurchased = true;
}
}
}

guiUpdater.triggerAsyncUpdate();
}

if (! havePricesBeenFetched)
{
havePricesBeenFetched = true; // [8]
juce::StringArray identifiers;

for (auto& voiceProduct : voiceProducts)
identifiers.add (voiceProduct.identifier);

juce::InAppPurchases::getInstance()->getProductsInformation (identifiers); // [9]
}
}
  • [6]サーバーからのレスポンスが正常であれば、購入リストを更新することができます。ヘルパー関数を使用して、識別子から音声のインデックスを見つけます。
  • [7]インデックスが有効であれば、VoiceProductオブジェクトの適切な変数を設定し、購入状態を反映させます。
  • [8]このコールバック関数が最初に呼ばれたときは、購入価格を初期化する必要があります。このコードセグメントが2回目に呼ばれないようにしてください。
  • [9]の商品価格を取得するリクエストを開始する。InAppPurchasesインスタンスを呼び出す。getProductsInformation()関数である。

これらのアプリ内課金商品のGUIを更新するためには、購入状態を保存する一時変数があると便利です。VoiceRowのサブクラスで、プライベートなメンバ変数として宣言しましょう。VoiceModelクラスである:

        bool isSelected = false, hasBeenPurchased = false, purchaseInProgress = false;
int rowSelected = -1;
juce::Image avatar;

現時点では、アプリ内課金商品はすべて同じビジュアルに見え、視覚的に印象的な方法で利用可能であることを明確に示していません。購入可能な商品には、異なるルック&フィールを適用してみましょう。アプリ内のpaint()関数が利用可能な場合は、商品画像に白い背景を適用するようにコードを修正する。[10]利用できない場合は、白い半透明のオーバーレイとなる[11]:

        void paint (juce::Graphics& g) override
{
auto r = getLocalBounds().reduced (4);
{
auto voiceIconBounds = r.removeFromLeft (r.getHeight());
g.setColour (juce::Colours::black);
g.drawRect (voiceIconBounds);

voiceIconBounds.reduce (1, 1);
g.setColour (hasBeenPurchased ? juce::Colours::white : juce::Colours::grey); // [10]
g.fillRect (voiceIconBounds);

g.drawImage (avatar, voiceIconBounds.toFloat());

if (! hasBeenPurchased)
{
g.setColour (juce::Colours::white.withAlpha (0.8f)); // [11]
g.fillRect (voiceIconBounds);

またupdate()関数を使用して、名前と価格のラベルに購入ステータスを反映させます。先に定義したヘルパー関数を使用して、まず行インデックスから VoiceProduct を取得します。[12]そして、その商品が購入されたかどうかを示す一時変数を設定する。[13].その後、以下のようにGUIを更新する:

        void update (int rowNumber, bool rowIsSelected)
{
isSelected = rowIsSelected;
rowSelected = rowNumber;

if (juce::isPositiveAndBelow (rowNumber, voices.size()))
{
auto imageResourceName = voices[rowNumber] + ".png";

nameLabel.setText (voices[rowNumber], juce::NotificationType::dontSendNotification);

auto purchase = purchases.getPurchase (rowNumber); // [12]
hasBeenPurchased = purchase.isPurchased; // [13]
                if (rowNumber == 0)
{
purchaseButton.setButtonText ("Internal");
purchaseButton.setEnabled (false);
}
else
{
purchaseButton.setButtonText (hasBeenPurchased ? "Purchased" : "Purchase");
purchaseButton.setEnabled (! hasBeenPurchased && purchase.priceIsKnown);
}

setInterceptsMouseClicks (! hasBeenPurchased, ! hasBeenPurchased); // [14]

このコードでは、主に、商品が購入されたときに名前と価格ラベルのフォントスタイルがデフォルトのままになるように更新し、色を白に変更しています。さらに、対応する商品が購入可能な場合、購入ボタンを有効にします。また、顧客への請求時のエラーを避けるために、購入後にこれらのボタンをマウスでクリックできないようにする必要があります。[14].

商品の購入

警告

このセクションは物理的なデバイス上でのみ機能します。シミュレーターでは正しく機能しませんので、試さないでください。

まだアプリに購買行動を実装していないので、実装しましょう。アプリにpurchaseVoice()パブリック関数VoicePurchasesクラスに転送します。InAppPurchasesインスタンスだ:

    void purchaseVoice (int voiceIndex)
{
if (havePricesBeenFetched && juce::isPositiveAndBelow (voiceIndex, voiceProducts.size()))
{
auto& product = voiceProducts.getReference (voiceIndex); // [1]

if (! product.isPurchased)
{
purchaseInProgress = true;

product.purchaseInProgress = true; // [2]
juce::InAppPurchases::getInstance()->purchaseProduct (product.identifier); // [3]
  • [1]まず、インデックスを使ってVoiceProductを取得する前に、価格が取得されているかどうかをチェックします。
  • [2]未購入の場合は、購入状態を変数に設定する。
  • [3]これで安心してInAppPurchasesインスタンスで、正しい識別子を指定して製品を購入する。

これは、購入が終了し、レスポンスを受信したときに、サーバーからのコールバックをトリガーします。これを実装します。productPurchaseFinished()コールバックはここにある:

    void productPurchaseFinished (const PurchaseInfo& info, bool success, const juce::String&) override
{
purchaseInProgress = false;

for (const auto& productId : info.purchase.productIds)
{
auto idx = findVoiceIndexFromIdentifier (productId); // [4]

if (juce::isPositiveAndBelow (idx, voiceProducts.size()))
{
auto& voiceProduct = voiceProducts.getReference (idx); // [5]

voiceProduct.isPurchased = success;
voiceProduct.purchaseInProgress = false;
}
else
{
// On failure Play Store will not tell us which purchase failed
for (auto& voiceProduct : voiceProducts)
voiceProduct.purchaseInProgress = false;
}
}

guiUpdater.triggerAsyncUpdate();
}

先ほどと同じヘルパー関数を使って、VoiceProductの識別子からインデックスを取得します。[4]そして、購入が成功したかどうかに応じて、当該オブジェクトに適切な変数を設定し、購入状態を終了する。[5].

サーバーからの応答を待つ間、ユーザーに購入状態を示すために、回転するアニメーションをpaint()の機能である。VoiceModelクラス[6]:

                if (! hasBeenPurchased)
{
g.setColour (juce::Colours::white.withAlpha (0.8f)); // [11]
g.fillRect (voiceIconBounds);

if (purchaseInProgress) // [6]
getLookAndFeel().drawSpinningWaitAnimation (g, juce::Colours::darkgrey,
voiceIconBounds.getX(),
voiceIconBounds.getY(),
voiceIconBounds.getWidth(),
voiceIconBounds.getHeight());
}
}
}

ユーザーがUIとインタラクトしたときに購入プロセスを開始するために、clickPurchase()関数を実装してみましょう:

        void clickPurchase()
{
if (rowSelected >= 0)
{
if (! hasBeenPurchased)
{
purchases.purchaseVoice (rowSelected); // [7]
purchaseInProgress = true;
startTimer (1000 / 50); // [8]
}
}
}

void timerCallback() override { repaint(); } // [9]
  • [7]もし行インデックスが有効で、以前に購入されたことがなければpurchaseVoice()関数を使用し、購買状態に入る。
  • [8]タイマーをスタートさせ、購入中に回転ホイールのアニメーションを更新します。
  • [9]アニメーションの画面を再描画するためのタイマー・コールバックを実装する。

最後にupdate()関数で、行固有の商品の購買状態を取得する。[10]を呼び出し、購入が完了したら回転アニメーションを停止する。stopTimer()機能[11]:

                purchaseInProgress = purchase.purchaseInProgress;                   // [10]

if (purchaseInProgress)
startTimer (1000 / 50);
else
stopTimer(); // [11]

チュートリアルのこの時点で、商品の購入と商品情報の取得を処理する仕組みはすべて実装されていますが、GUIに更新のタイミングを指示する必要があります。

非同期アップデートの処理

購入とIAPサーバーとの同期は別のスレッドで実行されるため、レスポンスを非同期で処理する必要がある。そのためMainContentComponentクラスを継承しよう。AsyncUpdaterクラス[1]このクラスの参照を VoicePurchases のインスタンスに渡す。[2]:

class MainContentComponent : public juce::Component,
private juce::AsyncUpdater // [1]
{
    juce::SoundPlayer player;
VoicePurchases purchases { *this }; // [2]
juce::AudioDeviceManager dm;

VoicePurchasesコンストラクタとメンバ初期化リストで、VoicePurchasesへの参照をAsyncUpdaterインスタンスをプライベート変数[3]:

    VoicePurchases (juce::AsyncUpdater& asyncUpdater)   // [3]
: guiUpdater (asyncUpdater)
{

を参照できるように、その変数をプライベート・メンバーとして宣言する。AsyncUpdater後に[4]:

    //==============================================================================
juce::AsyncUpdater& guiUpdater; // [4]

現在はpurchaseVoice()関数とInAppPurchasesすなわちproductsInfoReturned(),purchasesListRestored()そしてproductPurchaseFinished()対応するコードセグメントへの最後のステップとして、GUIへの非同期更新をトリガーする。[5]:

void purchaseVoice (int voiceIndex)
{
if (havePricesBeenFetched && juce::isPositiveAndBelow (voiceIndex, voiceProducts.size()))
{
//...

if (! product.isPurchased)
{
//...

guiUpdater.triggerAsyncUpdate(); // [5.1]
}
}
}

//...

void productsInfoReturned (const juce::Array& products) override
{
//...

guiUpdater.triggerAsyncUpdate(); // [5.2]
}

//...

void productPurchaseFinished (const PurchaseInfo& info, bool success, const juce::String&) override
{
//...

guiUpdater.triggerAsyncUpdate(); // [5.3]
}

//...

void purchasesListRestored (const juce::Array& infos, bool success, const juce::String&) override
{
if (success)
{
//...

guiUpdater.triggerAsyncUpdate(); // [5.4]
}

//...
}

これで、いつでもtriggerAsyncUpdate()関数が呼び出される。AsyncUpdaterインスタンスでコールバックを処理することができます。handleAsyncUpdate()古いGUIコンポーネントを更新する関数[6]:

    void handleAsyncUpdate() override
{
voiceListBox.updateContent();
voiceListBox.setEnabled (! purchases.isPurchaseInProgress());
voiceListBox.repaint();
}

以下のゲッターをVoicePurchasesクラスをパブリック・ヘルパー関数として使用する:

    bool isPurchaseInProgress() const noexcept { return purchaseInProgress; }

これにより、購入が行われたり取得されたりするたびにGUIが更新されるようになります。アプリを再度起動すると、以前に購入したアプリ内課金が表示されるようになります。

エクササイズ

ユーザーが特定の音声でフレーズを再生するたびにトークンを消費することで、これらのIAP製品の消費可能バージョンを実装します。

サウンドの再生

ユーザーが再生ボタンをクリックすると、正しい音声とフレーズを使って、正しいオーディオファイルを再生するようにトリガーする必要があります。オーディオファイルは、音声の名前とフレーズ番号に関連するインデックスを命名規則として使用し、バイナリリソースとして保存されます。このときMainContentComponentクラスでは、次のように振る舞いを処理する:

    void playStopPhrase()
{
juce::MemoryOutputStream resourceName;

auto idx = voiceListBox.getSelectedRow(); // [1]
if (juce::isPositiveAndBelow (idx, soundNames.size()))
{
resourceName << soundNames[idx] << phraseListBox.getSelectedRow() << ".ogg"; // [2]

auto dir = juce::File::getCurrentWorkingDirectory();

int numTries = 0;

while (! dir.getChildFile ("Resources").exists() && numTries++ < 15)
dir = dir.getParentDirectory();

auto file = dir.getChildFile ("Resources").getChildFile ("Sounds").getChildFile (resourceName.toString().toRawUTF8());

if (file.exists())
player.play (file); // [3]
}
}
  • [1]まず、音声テーブルの選択行のインデックスを取得し、そのインデックスが音声ファイルの配列に対して有効かどうかをチェックします。
  • [2]次に、正しいファイル名をMemoryOutputStreamオブジェクトと上記の命名規則に従っている。
  • [3]最後に、先に構築したファイル名に基づいてオーディオファイルをロードできるかどうかをチェックし、次のように呼び出します。play()の機能である。SoundPlayerを対応するファイルと一緒に保存する。

これで、再生ボタンを押すと、対応する音声とともに話し言葉が聞こえるはずです。

注記

この修正版のソースコードはInAppPurchaseTutorial_02.hファイルにある。

概要

このチュートリアルでは、モバイルとデスクトップでアプリ内課金を処理する方法を学びました。特に

  • さまざまな配備プラットフォームの予備セットアップをカバー。
  • 様々なIAP製品情報をユーザーインターフェースに表示。
  • 過去のユーザーの購入履歴を取得し、それに応じてGUIを非同期に更新。
  • アプリ内で消耗品以外の商品の購入に対応。

参照