Prainブログ

ゲーム開発とかIT小話とかその他雑記のブログ

*

SDKBoxを使ってアプリ内課金をするやり方 プログラム実装~実機テストまでの13の手順

      2016/10/11

sc_confirm_04

この記事では、SDKBoxを利用したiOSアプリ内課金のプログラム実装から実機テストのやり方まで、一連の流れで説明します。

 

 

アプリ課金の実機テストを、実際の費用を掛けずに行うためには以下の目次通りに作業を行う必要があります。

※目次上で「灰色」となっている手順は、別記事を参照の上、必要に応じて実施してください。

 

環境

XCode:7.3.1

Cocos2d-x:3.9

 

 

目次

  1. 事前準備
  2. プログラム実装、環境設定
  3. Apple Developer Programユーザの登録
  4. 開発用証明書、製品用証明書の取得
  5. APP IDの登録
  6. テスト用デバイスの登録
  7. 開発用プロビジョニングファイルの取得~実機テスト
  8. 口座情報、税金情報、コンタクトの登録
  9. Sandboxテスターの登録
  10. リリース用プロビジョニングファイルの取得
  11. リリースビルドの提出、審査
  12. 課金プロダクトの登録
  13. 課金の実機テスト
  14. まとめ
  15. 参考

 

 

スポンサーリンク

 

 

事前準備

SDKBoxのインストール、テスト課金用プロジェクトの作成、課金プラグインをインストールします。本手順については別記事を参照してください。

この記事では、アプリ課金のためのSDKBoxインストールのやり方を説明します。 環境XCode:7.3.1Cocos2d-x:3.9  SDKBoxについて通常、アプリ内の課金を実装するためには、AndroidならJava、iOSならばObjective-Cでそれぞれプログラムする必要がありま...

 

 

プログラム実装、環境設定

プログラム実装

KakinScene.h

#ifndef KakinScene_hpp
#define KakinScene_hpp

#include <stdio.h>
#ifdef SDKBOX_ENABLED
#include "PluginIAP/PluginIAP.h"
#endif


class KakinScene : public cocos2d::Layer, public sdkbox::IAPListener{
public:
    // there's no 'id' in cpp, so we recommend returning the class instance pointer
    static cocos2d::Scene* createScene();
    
    // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
    virtual bool init();
    
    // a selector callback
    void menuCloseCallback(cocos2d::Ref* pSender);
    void onShowAds(cocos2d::Ref* sender);
    void onRequestIAP(cocos2d::Ref* sender);
    void onRequestIAPKakin(cocos2d::Ref* sender);
    void onRestoreIAP(cocos2d::Ref* sender);
    void onIAP(cocos2d::Ref* sender);

    
    // implement the "static create()" method manually
    CREATE_FUNC(KakinScene);
    
private:
    
    void updateIAP(const std::vector<sdkbox::Product>& products);
    
    virtual void onSuccess(sdkbox::Product const& p) override;
    
    virtual void onFailure(sdkbox::Product const& p, const std::string &msg) override;
    
    virtual void onCanceled(sdkbox::Product const& p) override;
    
    virtual void onRestored(sdkbox::Product const& p) override;
    
    virtual void onProductRequestSuccess(std::vector<sdkbox::Product> const &products) override;
    
    virtual void onProductRequestFailure(const std::string &msg) override;
    
    virtual void onInitialized(bool success) override;
    
    virtual void onRestoreComplete(bool ok, const std::string &msg) override;
    
    cocos2d::CCMenu* _iapMenu;
    std::vector<sdkbox::Product> _products;
    cocos2d::Label* _txtCoin;
    int _coinCount;
    
    void viewProductsLog(std::string str);
    
};


#endif /* KakinScene_hpp */

 

 

KakinScene.cpp

#include "KakinScene.hpp"
USING_NS_CC;

template <typename T> std::string tostr(const T& t) { std::ostringstream os; os<<t; return os.str(); }

Scene* KakinScene::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();
    
    // 'layer' is an autorelease object
    auto layer = KakinScene::create();
    
    // add layer as a child to scene
    scene->addChild(layer);
    
    // return the scene
    return scene;
}

// on "init" you need to initialize your instance
bool KakinScene::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
    auto dirs = Director::getInstance();
    Size visibleSize = dirs->getVisibleSize();
    Vec2 origin = dirs->getVisibleOrigin();
    
    // コイン
    _coinCount = 0;
    _txtCoin = Label::create("0", "Marker Felt.ttf", 32);
    _txtCoin->setAnchorPoint(cocos2d::Point(0, 0));
    _txtCoin->setPosition(cocos2d::Point(50, 50));
    addChild(_txtCoin);
    
    // In-App Purchase Demoラベル
    auto menuNode = Node::create();
    menuNode->setName("menuNode");
    int index = 2;
    
    auto thisSceneLabel = Label::createWithTTF("In-App Purchase Demo", "fonts/Marker Felt.ttf", 32);
    thisSceneLabel->setPosition(Vec2(origin.x+visibleSize.width/2, origin.y +visibleSize.height/2).x, (Vec2(origin.x+visibleSize.width/2, origin.y+visibleSize.height).y - (index) * 40));
    
    this->addChild(thisSceneLabel, 1);
    
    
    // 各種初期化
    sdkbox::IAP::init();
    sdkbox::IAP::setDebug(true);
    sdkbox::IAP::setListener(this);
    
    // クライアントサイドでのレシートの検証を可能にする
    // note: iOSは購入レシートを提供しない、暗号化されたペイロード(データ)のみ
    sdkbox::IAP::enableUserSideVerification(true);
    
    // 広告削除ボタン
    auto menuItem1 = MenuItemFont::create("Remove Ads");
    menuItem1->setFontNameObj("Marker Felt.ttf");
    menuItem1->setFontSizeObj(32);
    menuItem1->setName("menuItem1");
    menuItem1->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2));
    menuItem1->setCallback([&](cocos2d::Ref *sender)
                           {
                               onRequestIAP(sender);
                           });
    auto menu = Menu::create(menuItem1, NULL);
    menu->setName("menu");
    menuNode->addChild(menu, 1);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menuNode, 2);
    
    
    
    // 課金ボタン
    auto menuItem2 = MenuItemFont::create("Kakin");
    menuItem2->setFontNameObj("Marker Felt.ttf");
    menuItem2->setFontSizeObj(32);
    menuItem2->setName("menuItem2");
    menuItem2->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2 - 50));
    menuItem2->setCallback([&](cocos2d::Ref *sender)
                           {
                               onRequestIAPKakin(sender);
                           });
    auto menu2 = Menu::create(menuItem2, NULL);
    menu2->setName("menu");
    auto menuNode2 = Node::create();
    menuNode2->addChild(menu2, 1);
    menu2->setPosition(Vec2::ZERO);
    this->addChild(menuNode2, 2);
    
    auto closeItem = MenuItemFont::create("CloseNormal.png",
                                          [&](Ref* sender){
                                              // your code here
                                          });
    
    
    return true;
}

void KakinScene::onRequestIAP(cocos2d::Ref* sender)
{
    sdkbox::IAP::purchase("remove_ads");
}


void KakinScene::onRequestIAPKakin(cocos2d::Ref* sender){
    sdkbox::IAP::purchase("coin_package");
}

void KakinScene::onShowAds(cocos2d::Ref *sender)
{
    CCLOG("Show Ads");
}

void KakinScene::onRestoreIAP(cocos2d::Ref* sender)
{
    sdkbox::IAP::restore();
}

void KakinScene::onIAP(cocos2d::Ref *sender)
{
    auto btn = static_cast<Node*>(sender);
    sdkbox::Product* p = (sdkbox::Product*)btn->getUserData();
    
    CCLOG("Start IAP %s", p->name.c_str());
    sdkbox::IAP::purchase(p->name);
}

void KakinScene::menuCloseCallback(Ref* pSender)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
    MessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert");
    return;
#endif
    
    Director::getInstance()->end();
    
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
    exit(0);
#endif
}

/*
 購入処理が成功した際に呼び出される
 */
void KakinScene::onSuccess(const sdkbox::Product &p)
{
    if (p.name == "coin_package") {
        _coinCount += 1000;
        _txtCoin->setString(tostr(_coinCount));
        
        viewProductsLog("coin_package");
        
    }
    else if (p.name == "coin_package2") {
        _coinCount += 5000;
        _txtCoin->setString(tostr(_coinCount));
    }
    else if (p.name == "remove_ads") {
        CCLOG("Remove Ads");
        
        viewProductsLog("remove_ads");
        
    }
    

    CCLOG("Purchase Success: %s", p.id.c_str());
}

/*
 購入直前でキャンセルを押下した場合に呼び出される
 */
void KakinScene::onFailure(const sdkbox::Product &p, const std::string &msg)
{
    CCLOG("Purchase Failed: %s", msg.c_str());
}

/*

 */
void KakinScene::onCanceled(const sdkbox::Product &p)
{
    CCLOG("Purchase Canceled: %s", p.id.c_str());
}

/*
 購入済みのプロダクトの復元に完了した際に呼び出される
 */
void KakinScene::onRestored(const sdkbox::Product& p)
{
    CCLOG("Purchase Restored: %s", p.name.c_str());
}

void KakinScene::updateIAP(const std::vector<sdkbox::Product>& products)
{
    //
    //    _iapMenu->removeAllChildren();
    //    _products = products;
    //
    //    int posX = 0;
    //    int posY = 0;
    //
    //    for (int i=0; i < _products.size(); i++)
    //    {
    //        CCLOG("iap: %s", _products[i].name.c_str());
    //
    //        auto btn = Label::create();
    //        btn->setString(_products[i].name.c_str());
    ////        auto btn = CCMenuItemFont::create(_products[i].name.c_str(), CC_CALLBACK_1(HelloWorld::onIAP, this) );
    //        btn->setColor(Color3B::WHITE);
    //        btn->setUserData(&_products[i]);
    //        btn->setPosition(CCPoint(posX, posY));
    //        _iapMenu->addChild(btn);
    //        posY += 50;
    //    }
}

/*
 最新のプロダクトへの要求が成功した際に呼び出される
 */
void KakinScene::onProductRequestSuccess(const std::vector<sdkbox::Product> &products)
{
    CCLOG("called onProductRequestSuccess");
    updateIAP(products);
    
}

/*
 最新のプロダクトへの要求が失敗した際に呼び出される
 */
void KakinScene::onProductRequestFailure(const std::string &msg)
{
    CCLOG("Fail to load products");
}

/*
 
 */
void KakinScene::onInitialized(bool success){
    CCLOG("%s:%d", __func__, success);
}

/*
 プロダクトの復元が完了した際に呼び出される
 */
void KakinScene::onRestoreComplete(bool ok, const std::string &msg){
    CCLOG("%s:%d:%s", __func__, ok, msg.data());
    
    if(ok){
        CCLOG("Restore Successful: %s", msg.c_str());
    }else{
        CCLOG("Restore Failed: %s", msg.c_str());
    }
    
}


/*
 
 */
void KakinScene::viewProductsLog(std::string str){
    
    // プロダクトの更新
    CCLOG("called refresh");
    sdkbox::IAP::refresh();
    _products = sdkbox::IAP::getProducts();
    
    // プロダクトの中身の確認
    for(int i = 0; i < 4; i++){
        if(str == _products[i].name){
            CCLOG("/◼️*******************************************/");
            // The name specified in sdkbox_config.json
            std::string name;
            name = _products[i].name;
            CCLOG("name = %s", name.c_str());
            
            // The product id of an In App Purchase
            std::string id;
            id = _products[i].id;
            CCLOG("id = %s", id.c_str());
            
            // Type of iap item
            sdkbox::IAP_Type type;
            
            // The title of the IAP item
            std::string title;
            title = _products[i].title;
            CCLOG("title = %s", title.c_str());
            
            // The description of the IAP item
            std::string description;
            description = _products[i].description;
            CCLOG("description = %s", description.c_str());
            
            // Price value in float
            float priceValue;
            priceValue = _products[i].priceValue;
            CCLOG("priceValue = %f", priceValue);
            
            // Localized price
            std::string price;
            price = _products[i].price;
            CCLOG("price = %s", price.c_str());
            
            // price currency code
            std::string currencyCode;
            currencyCode = _products[i].currencyCode;
            CCLOG("currencyCode = %s", currencyCode.c_str());
            
            // cyphered payload
            std::string receiptCipheredPayload;
            receiptCipheredPayload = _products[i].receiptCipheredPayload;
            CCLOG("receiptCipheredPayload = %s", receiptCipheredPayload.c_str());
            
            // receipt info. will be empty string for iOS
            std::string receipt;
            receipt = _products[i].receipt;
            CCLOG("receipt = %s", receipt.c_str());
            
            // unique transaction id
            std::string transactionID;
            transactionID = _products[i].transactionID;
            CCLOG("transactionID = %s", transactionID.c_str());
            
            CCLOG("/*******************************************◼️/");
        }
    }

    
    
}

 

 

sdkbox_config.json(72行目以降を下記に置き換え)

  中略
    "ios": {
        "iap": {
            "items": {
                "remove_ads": {
                    "type": "non_consumable", 
                    "id": "com.prain.practicesdkboxiap.non1"
                }, 
                "double_coin": {
                    "type": "non_consumable", 
                    "id": "com.cocos2dx.non2"
                }, 
                "coin_package": {
                    "id": "com.prain.practicesdkboxiap.plugintest2"
                }, 
                "coin_package2": {
                    "id": "com.prain.practicesdkboxiap.plugintest3"
                }
            }
        }
    }
}

 

AppDelegate.cpp(ヘッダ情報を追加)


#include "KakinScene.hpp"

 

AppDelegate.cpp(HelloWorld::createScene()をKakinScene::createScene()に置き換え)

    // create a scene. it's an autorelease object
    auto scene = KakinScene::createScene();

 

 

 

環境設定

・info.plistに以下のエントリを追加する

下記の赤枠のようにinfo.plistを編集します。

sdkbox_iap_config_03

info.plistはFinder上では「プロジェクト->proj.ios_mac->ios」配下にあります。編集後はXcode上では下記のように見えます。

Xcode

“NS”の文字が表示されていませんが、特に問題ないようです。

 

 

・bitcodeを無効化する

「Xcode->プロジェクト->PROJECT->Build Settings->Build Options(iOS)」から「Enable Bitcode」を「No」にします。

bitcode

 

 

・フルスクリーンにチェックを入れる

「Xcode->プロジェクト->TARGETS->General->Deplyment Info」の「Requires full screen」にチェックを入れます。

full screen

 

 

また、プログラムを実装するにあたり、こちらが参考になりました。

 

I have install sdkbox and iap plugins, but I dont know how I integrate to my project. I have try like no ads example in video but when I add sdkbox::IAPListener my CREATE_FUNC gives error (Allocating an object of abstract class type 'GameScen...

 

 

 

 

Apple Developer Programユーザの登録

本手順については別記事を参照してください。

この記事では、iOSアプリを作成した後、実機でテストしてAppStoreにリリースするまでの一連の手順を紹介します。 手順を検索すると色々なサイトが出てくるのですが、情報が古くなってしまったり散在していたりするため、自分の備忘も兼ねて、まとめて本記事に記...

 

 

 

開発用証明書、製品用証明書の取得

本手順については別記事を参照してください。

この記事では、iOSアプリを作成した後、実機でテストしてAppStoreにリリースするまでの一連の手順を紹介します。 手順を検索すると色々なサイトが出てくるのですが、情報が古くなってしまったり散在していたりするため、自分の備忘も兼ねて、まとめて本記事に記...

 

 

 

APP IDの登録

本手順については別記事を参照してください。

この記事では、iOSアプリを作成した後、実機でテストしてAppStoreにリリースするまでの一連の手順を紹介します。 手順を検索すると色々なサイトが出てくるのですが、情報が古くなってしまったり散在していたりするため、自分の備忘も兼ねて、まとめて本記事に記...

 

 

 

テスト用デバイスの登録

本手順については別記事を参照してください。

この記事では、iOSアプリを作成した後、実機でテストしてAppStoreにリリースするまでの一連の手順を紹介します。 手順を検索すると色々なサイトが出てくるのですが、情報が古くなってしまったり散在していたりするため、自分の備忘も兼ねて、まとめて本記事に記...

 

 

 

開発用プロビジョニングファイルの取得~実機テスト

本手順については別記事を参照してください。

この記事では、iOSアプリを作成した後、実機でテストしてAppStoreにリリースするまでの一連の手順を紹介します。 手順を検索すると色々なサイトが出てくるのですが、情報が古くなってしまったり散在していたりするため、自分の備忘も兼ねて、まとめて本記事に記...

 

 

※まだ課金プロダクトをiTuneConnectに登録していないので、ここでの実機テストは、あくまでクライアントの振る舞いのテストのみを行います。具体的には、課金ラベルを押下してもエラーが返ってくることを確認します。

 

 

口座情報、税金情報、コンタクトの登録

初めて課金テストをする場合、テストに先立ってiTuneConnectに口座情報等を登録しておく必要があります。

 

こちらの記事が参考になりました。

 

AppStoreに有料アプリ、iAdアプリを公開するためには、 iTunes Connectで契約者、銀行口座…

 

 

 

Sandboxテスターの登録

課金テスト用のユーザを登録します。このユーザを使用することで実際の費用を掛けずにテストをすることができます。

 

テストユーザの追加

1.「iTuneConnect->ユーザと役割」からユーザを登録します。sc_sandbox_00

2.「Sandboxテスター」に移動し「+」をクリックします。sc_sandbox_01

 

3.テストユーザの情報を入力します。テストユーザにはメールアドレスが必要になります。sc_sandbox_02

 

 

リリース用プロビジョニングファイルの取得

本手順については別記事を参照してください。

この記事では、iOSアプリを作成した後、実機でテストしてAppStoreにリリースするまでの一連の手順を紹介します。 手順を検索すると色々なサイトが出てくるのですが、情報が古くなってしまったり散在していたりするため、自分の備忘も兼ねて、まとめて本記事に記...

 

 

 

リリースビルドの提出

本手順については別記事を参照してください。

この記事では、iOSアプリを作成した後、実機でテストしてAppStoreにリリースするまでの一連の手順を紹介します。 手順を検索すると色々なサイトが出てくるのですが、情報が古くなってしまったり散在していたりするため、自分の備忘も兼ねて、まとめて本記事に記...

 

 

 

課金プロダクトの登録

課金プロダクトをiTuneConnectから登録します。プロダクトIDはアプリ側で実装したものと同じにする必要があります。

 

非消耗型プロダクトの追加

1.「iTuneConnect->マイApp->提出したリリースビルド名」から、機能タブ画面から「+」をクリックします。

sc_product_00

2.「非消耗型」を選択します。sc_product_01

 

 

3.参照名、製品ID、価格を入力します。製品IDはsdkbox_config.jsonに入力したものと同じものを入力します。下図は“remove_ads”の

"id": "com.prain.practicesdkboxiap.non1"を入力しています。

sc_product_02

 

 

4.「言語」を追加します。

sc_product_03sc_product_04

 

 

5.「Appleでコンテンツをホスティングする」で「いいえ」をチェックします。

sc_product_05

 

 

6.「審査メモ(オプション)」、「審査のためのスクリーンショット」は実際にアプリをリリースする際に入力します。今回はそのまま「Save」して完了です。

 

 

消耗型プロダクトの追加

同様にして消耗型プロダクトを追加します。今度は「消耗型」を選択して、下図のように情報を入力してください。

sc_product_06

 

sc_product_07

 

 

確認

作業後、下図のように登録されていればOKです。

sc_product_08

 

 

「Appleでコンテンツをホスティングする」については下記の記事が参考になります。

iOSアプリ内課金についての記事です。 前置き iOSのアプリ内課金を実装するとき、プロダクト(課金アイテム)をどのように提供するか、検討が必要になります。そうはいっても採択できる方法は次の2つになるのですが。 プロダクトをアプリに内包し課金後それが使用で...

 

 

 

Tips:製品IDは小文字で登録すると、Androidでも流用できて管理が楽になるようです。(Androidは大文字が利用できないみたいです。)

 

 

実機テスト

ここまで来てようやく実機テストです。長かったですね。

 

テストの前に現在サインインしているAppleIDをサインアウトする

iPhone、iPadから「設定->iTunes & App Store->Apple ID->サインアウト」でサインアウトしておきます。

 

 

非消耗型プロダクトの購入

1.「Remove Ads」をタップします。

sc_confirm_00

 

2.「購入する」をタップします。

sc_confirm_01

 

3.「既存のApple IDを使用」をタップします。

sc_confirm_02_01

 

4.Sandboxテスターを登録するで登録したメールアドレスとパスワードを入力します。

sc_confirm_02_02

 

5.購入が完了しました。
sc_confirm_03

 

 

消耗型プロダクトの購入

同様に消耗型プロダクトをテストします。

1.「Kakin」をタップします。

sc_confirm_04

 

2.購入が完了しました。画面左下の数値が1000になっています。

※ちなみに何回も購入すればどんどん増えていきます。テストとはいえ、なんだか面白くなってきます。

sc_confirm_05

 

 

非消耗型プロダクトの再度の購入

非消耗型プロダクトを再度購入した場合の振る舞いを確認します。「Remove Ads」をタップして再度購入してみてください。

sc_confirm_06 sc_confirm_07 sc_confirm_08

 

 

まとめ

SDKBoxで課金プロダクトを実装して実機テストするためには以下の3点がポイントとなると思います。

・アプリ内課金の種類を理解する。

・SDKBoxが提供するAPI群を理解する。

・アプリ側とiTuneConnect側で同じプロダクトIDを実装、登録する。

 

 

(※重要)本記事は、単純な正常系の課金テストのみを行っているものです。

実際の課金処理では想定される不具合、不正に対してセキュアな実装がなされるべきであり、本手順はそれらを保証するものではなく、すべての不具合に対して責任を負いません。

 

 

参考

課金処理の基礎知識

2015/2/11 追記指パッチンで離れた距離からでもシャッターが切れるカメラアプリ SnapCaemeraをリリースしました。無料のアプリです。ダウンロードよろしくお願いします。iPhoneのアプリ内課金について調べたことを自分用メモしときます。Q.今iPhoneで出来る課金って...

 

もう少し詳しい課金処理の基礎知識

# 参考(https://developer.apple.com/jp/documentation/StoreKitGuide.pdf)...

 

 

以上です。ご覧いただきありがとうございました。

 

 - , ,

        

Message

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

  関連記事

SDKBoxを使ってアプリ内課金をするやり方 インストール編

この記事では、アプリ課金のためのSDKBoxインストールのやり方を説明します。 …

シミュレータ
ゲーム制作 Cocos2d-x関連 第3回 「画像の表示処理 解説」

今回はCocos2d-xにおける画像表示コードの解説です。   スポン …

動作確認画像
Cocos2d-x アプリ起動画面(スプラッシュ画像)の変更方法について

この記事では、アプリの起動画面(スプラッシュ画像)の変更のやり方について説明しま …

遊びを繋げ新たな地平へ プレイン Prain
ゲーム制作 Cocos2d-x関連 第9回 「移動しているボールの減速」

この辺りで大体折り返し地点です。   さて、弾いたボールはだんだんと減 …

ゲーム制作 Cocos2d-x関連 第16回 「パズル要素を実装する」

ちょっと待って、パズル要素はどこなの???   ・・・ですよね・・・「 …