четверг, 7 февраля 2013 г.

in app purchase cocos2d-x (покупки внутри приложения)


В этой статье я расскажу как написать код для  IN APP PURCHASE в COCOS 2d-x. (разработка   под MacOS на XCode 4.5)


Чтобы не было реплик, типа статья украдена и т.д. скажу:
Эта статья написана мной, она является результатом моих поисков решения проблемы с in app purchase для cocos2d-x. Естественно я перечитал массу западных форумов, потратил не один день,  в моей статье используются куски кода с этих форумов, но в отличии от них, эти "кусочки" правильно соеденены и работают, + добавленный мной код.


Как это работает... В интернете куча статей, про то как работает биллинг, однако постараюсь все-же написать пару слов об этом, чтобы было понятно.
Если вы хотите продавать контент (далее "продукты") внутри своей игры, то придется писать in app purchase. Задача не совсем сложная, но литературы на русском языке практически нет, а на английском, в основном, статьи для cocos2d objective-c и все они разрозненные и практически не работают.

Нам надо реализацию на C++, а не на Objective-C. Компилятор Objective-C и среда XCode позволяют писать на C++, но файл с реализацией класса должен иметь расширение .mm
Я нарисовал схему, взаимодействия и наследования классов, для реализации In app purchase:


StoreKit - apple framework для реализации IAP. Ее мы будем использовать.
IAPHelper_objc - это пользовательскй класс, который взаимодействует с StoreKit, для этого он реализует два протокола <SKProductsRequestDelegateSKPaymentTransactionObserver>,  мы используем синглтон данного класса. Написан он на objective c.
IAPWrapper - это класс - мост, между IAPHelper_objc и нашим C++ классом, который реализует ф-ции магазина в приложении. В моей игре этот класс (CMyShop) унаследован от CCLayer.
IAPCallback - это виртуальный класс, написан на C++. От него, так же, будет унаследован наш класс магазин (CMyShop) для получения ответов от сервера.
Как все работает...
Пользователь заходит в наш виртуальный магазин (объект класса CMyShop), в нем через объект класса IAPWrapper отсылаются запросы на сервер apple. Когда приходят асинхронные ответы от сервера, то вызываются соответствующие методы нашего класса- магазина, т.к. он реализует методы виртуального класса IAPCallback. В этих методах мы и обрабатываем покупки, неудачи и т.д.

Он сказал поехали и запил водой...

Шаг 1.Начнем отсюда: http://habrahabr.ru/post/84359/ В этой статье не плохо описан сам принцип биллинга и то, как в консоли разработчика создать запись о своей игре, добавить тестовых пользователей и "продукты", которые будем продавать. Без этого ничего не заработает.

Шаг 2. Надеюсь с первым шагом проблем не возникло, можно продолжать...
ИТАК... опишу бизнес процесс...
за биллинг отвечает библиотека StoreKit, которая  входит в набор стандартных библиотек для разработки под iOS, от компании Apple.
Значит ее необходимо добавить в свой проект: в Project Navigator, кликните на самом верхнем элементе дерева, это ваш проект. На панели редактора откроется окно с информацией и установками вашего проекта. Идем на вкладку Summary, прокручиваем окно вниз, находим окошко Linked Frameworks and Libraries, там внизу нажимаем на "+", добавляем StoreKit.framework.

Для осуществления биллинга я создал, (больше украл) несколько классов:

1) IAPHelper_objc -  это класс, который, непосредственно, взаимодействует с StoreKit.
Когда пользователь в вашей игре пытается что-то купить, storekit отправляет запрос на сервер apple, там он обрабатывается и возвращается ответ... За все это отвечает IAPHelper_objc.
IAPHelper_objc -  написан на objective_c.  Он фактически посылает запросы на сервер и получает ответы, а также "покупает продукты". Для всех нас, естественно, очевидно, что получать ответы от сервера класс должен асинхронно. А раз асинхронно, значит должен быть реализован механизм обратного вызова ф-ций...  Для реализации этой функциональности наш IAPHelper_objc должен имплементировать два протокола (это понятие obj-c, а на простом языке, это интерфейсы):

<SKProductsRequestDelegateSKPaymentTransactionObserver>


SKProductsRequestDelegate - это протокол для отправки запросов и получения ответов от сервера.
В нем объявлен всего один метод, этот метод вызывается после получения ответа от сервера.

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response 
в этом ответе, в переменной,  response  приходит список доступных продуктов.

SKProductsRequest,  SKProductsResponse - эти типы, запроса и ответа, описаны в библиотеке StoreKit.


SKPaymentTransactionObserver - это протокол для осуществления покупок.

класс IAPHelpet_objc лучше  поместить в подгруппу ios  нашего проекта.

Создание синглтона IAPHelper_objc должно быть в методе didFinishLaunchingWithOptions,класса AppController. Он находится в группе ios, нашего проекта.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
  ...
 // In-app purchases: the helper will be notified when the product purchase transactions come in
    [[SKPaymentQueue defaultQueue] addTransactionObserver:[IAPHelper_objc sharedHelper]];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(productPurchased:)      name:kProductPurchasedNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(productPurchaseFailed:) name:kProductPurchaseFailedNotification object: nil];
}
Вот заголовок  класса IAPHelper_objc.h
//
//
//  IAPHelper_objc.h
//  Truck Adventure
//
//

#ifndef Truck_Adventure_IAPHelper_objc_h
#define Truck_Adventure_IAPHelper_objc_h

#import <foundation foundation.h>
#import "StoreKit/StoreKit.h"
#include "IAPCallback.h"

#define kProductsLoadedNotification         @"ProductsLoaded"
#define kProductPurchasedNotification       @"ProductPurchased"
#define kProductPurchaseFailedNotification  @"ProductPurchaseFailed"

@interface IAPHelper_objc : NSObject  {
    NSSet * _productIdentifiers;
    NSArray * _products;
    NSMutableSet * _purchasedProducts;
    SKProductsRequest * _request;
    iOSBridge::Callbacks::IAPCallback* latestIAPCallback;
}

@property (retain) NSSet *productIdentifiers;
@property (retain) NSArray * products;
@property (retain) NSMutableSet *purchasedProducts;
@property (retain) SKProductsRequest *request;

- (void)requestProducts;
- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers;
- (bool)canMakePayments;
- (void)requestProductsWithCallback:(iOSBridge::Callbacks::IAPCallback*)callback;
- (void)buyProductIdentifier:(NSString *)productIdentifier;
- (int)getProductIndexByIdentifier:(NSString*)productIdentifier;
+(id) sharedHelper;
//- (void)buyProductIdentifier:(NSString *)productIdentifier;

@end

#endif

А вот и реализация
//
//  IAPHelper_objc.mm
//  Truck Adventure
//
//

#import "IAPHelper_objc.h"
#include "Const.h"

@interface IAPHelper_objc () 
@end

@implementation IAPHelper_objc 

@synthesize productIdentifiers = _productIdentifiers;
@synthesize products = _products;
@synthesize purchasedProducts = _purchasedProducts;
@synthesize request = _request;


+ (IAPHelper_objc *)sharedHelper
{
    static dispatch_once_t once;
    static IAPHelper_objc * sharedHelper;
    dispatch_once(&once,
        ^{
                NSSet * productIdentifiers = [NSSet setWithObjects:
                                              @"IDProduct1", // наш продукт 1
                                              @"IDProduct2", //  наш продукт 2
                                            nil];
                sharedHelper = [[self alloc] initWithProductIdentifiers:productIdentifiers];
        }
    );
    return sharedHelper;
}

- (id)initWithProductIdentifiers:(NSSet *)productIdentifiers{
 latestIAPCallback = nil;
    
 if ((self = [super init])) {
  _productIdentifiers = [productIdentifiers retain];
        
  // Check for previously purchased products
  NSMutableSet * purchasedProducts = [NSMutableSet set];
  for (NSString * productIdentifier in _productIdentifiers)
  {
   BOOL productPurchased = [[NSUserDefaults standardUserDefaults] boolForKey:productIdentifier];
   if (productPurchased)
   {
    [purchasedProducts addObject:productIdentifier];
    NSLog(@"Previously purchased: %@", productIdentifier);
   }
   NSLog(@"Not purchased: %@", productIdentifier);
  }
        
  self.purchasedProducts = purchasedProducts;
 }
 return self;
}

- (bool)canMakePayments
{
 return [SKPaymentQueue canMakePayments];
}

- (void)requestProductsWithCallback:(iOSBridge::Callbacks::IAPCallback*)callback
{
 latestIAPCallback = callback;
 [self requestProducts];
}

-(void) setNullCallback
{
    latestIAPCallback = nil;
}
- (void)requestProducts
{
 self.request = [[[SKProductsRequest alloc] initWithProductIdentifiers:_productIdentifiers] autorelease];
 _request.delegate = self;
 [_request start];
    
}

// productsRequest это ф-ция обратного вызова (протокол SKProductsRequestDelegate) , которая начинает работать после получения ответа от сервера
// запрос к серверу отправляет ф-ция requestProduct
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    
    NSLog(@"Received products results...");
    self.products = response.products;
    self.request = nil;
    
    [[NSNotificationCenter defaultCenter] postNotificationName:kProductsLoadedNotification object:_products];
    
    if (self.products.count>0)
    {
        CCArray* items = CCArray::create();
        items->retain();
        
        NSArray * skProducts = response.products;
        for (SKProduct * skProduct in skProducts) {
            NSLog(@"Найден продукт: %@ %@ %0.2f",
                  skProduct.productIdentifier,
                  skProduct.localizedTitle,
                  skProduct.price.floatValue);
            iOSBridge::Callbacks::IAPItem* item = new iOSBridge::Callbacks::IAPItem();
            item->identification = skProduct.productIdentifier.UTF8String;
            item->name = std::string("noname");
            item->localizedDescription = skProduct.localizedDescription.UTF8String;
            item->localizedTitle = skProduct.localizedTitle.UTF8String;
            items->addObject(item);
        }
        if (latestIAPCallback)
            latestIAPCallback->productsDownloaded(items);
    }
    else if (latestIAPCallback)
            latestIAPCallback->productDonwloadFailed();
}

- (void)productsLoaded:(NSNotification *)notification {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
}

- (void)recordTransaction:(SKPaymentTransaction *)transaction {
    // Optional: Record the transaction on the server side...
}

- (void)provideContent:(NSString *)productIdentifier {
    
    NSLog(@"Toggling flag for: %@", productIdentifier);
    [[NSUserDefaults standardUserDefaults] setBool:TRUE forKey:productIdentifier];
    [[NSUserDefaults standardUserDefaults] synchronize];
    [_purchasedProducts addObject:productIdentifier];
    
    //[[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchasedNotification object:productIdentifier];
    
}

- (void)completeTransaction:(SKPaymentTransaction *)transaction {
    
    NSLog(@"completeTransaction...");
    
    [self recordTransaction: transaction];
   // 
    [self provideContent: transaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    NSString *product = transaction.payment.productIdentifier;
    std::string stdproduct =  std::string([product UTF8String]);
    if (latestIAPCallback) 
        latestIAPCallback->productPurchased(stdproduct);
    else
        myconst->bonus+=10000; //  завершаем покупку, если вышли из магазина
}

- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    
    NSLog(@"restoreTransaction...");
    
    [self recordTransaction: transaction];
    [self provideContent: transaction.originalTransaction.payment.productIdentifier];
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    NSString *product = transaction.payment.productIdentifier;
    std::string stdproduct =  std::string([product UTF8String]);
    if (latestIAPCallback)
        latestIAPCallback->productRestored(stdproduct);
}

- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    
    if (transaction.error.code != SKErrorPaymentCancelled)
    {
        NSLog(@"Transaction error: %@", transaction.error.localizedDescription);
    }
    
    //[[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchaseFailedNotification object:transaction];
    
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    NSString *product = transaction.payment.productIdentifier;
    std::string stdproduct =  std::string([product UTF8String]);
    if (latestIAPCallback)
        latestIAPCallback->productCanceled(stdproduct);
}

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased:
                [self completeTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:
                [self restoreTransaction:transaction];
            default:
                break;
        }
    }
}

- (void)buyProductIdentifier:(NSString *)productIdentifier
{
    NSLog(@"Покупаем %@...", productIdentifier);
    
    for (SKProduct * product in _products)
    {
        if ([product.productIdentifier isEqualToString:productIdentifier])
        {
            SKPayment * payment = [SKPayment paymentWithProduct:product];
            [[SKPaymentQueue defaultQueue] addPayment:payment];
            break;
        }
    }
}

- (int)getProductIndexByIdentifier:(NSString*)productIdentifier
{
    NSLog(@"Ищем индекс для %@...", productIdentifier);
    int index=0;
    bool bFound = NO;
    for (SKProduct * product in _products)
    {
        if ([product.productIdentifier isEqualToString:productIdentifier]) {
            bFound=YES;
            break;
        }
        index++;
    }
    if (!bFound) index=-1;
    return index;
}


- (void)dealloc
    {
        [_productIdentifiers release];
        _productIdentifiers = nil;
        [_products release];
        _products = nil;
        [_purchasedProducts release];
        _purchasedProducts = nil;
        [_request release];
        _request = nil;
        [super dealloc];
    }
        
@end




не буду умничать, я сам не вникал в некоторые места этого кода. Разберетесь сами, если будет нужно. Просто скопируйте код класса в свой проект. Не забудьте изменить в методе sharedHelper ID-шники ваших продуктов, созданных на шаге 1. В данном случае - это IDProduct1 и IDProduct2.

Дальше смотрим класс IAPCallback, как уже упоминалось раньше, он написан на C++

#ifndef xxx_IAPCallback_h
#define xxx_IAPCallback_h

#include "cocos2d.h"


using namespace cocos2d;

namespace iOSBridge
{
 namespace Callbacks
 {
  class IAPItem : public CCObject
  {
        public:
   std::string identification;
   std::string name;
   std::string localizedTitle;
   std::string localizedDescription;
   float price;
  };
        
  class IAPCallback
  {
  public:
            // Callback когда загружена инфа о продуктах с itunes сервера
     virtual void productsDownloaded(CCArray* products) = 0;
           
            // Callback когда не загружена инфа о продуктах с itunes сервера
     virtual void productDonwloadFailed() = 0;
           
            // Callback когда транзакция успешна
            virtual void productPurchased(std::string productID)=0;
            
            // Callback когда транзакция восстановлена
            virtual void productRestored(std::string productID)=0;
            
            // Callback когда транзакция неуспешна
            virtual void productCanceled(std::string productID)=0;
            
    };
        

 }
}

#endif
Стоит отметить класс IAPItem - это запись об одном продукте, унаследован он CCObject, поэтому его можно использовать для работы с  CCArray.
В IAPCallback объявлено несколько виртуальных методов,  реализация которых в классе - потомке "магазин" (CMyShop) . Думаю назначение методов понятно из комментов.  Они вызываются асинхронно, при возникновении событий во время биллинга.

Дальше смотрим IAPWrapper. Наш мостик между C++ и Objective c. Для отправки запросов на сервер в классе - магазине, вызываются методы объекта класса IAPWrapper. А в этих методах вызовы перенапрявляются синглтону IAPHelper_objc. Файл имплементации IAPWrapper должен иметь расширение mm. Кстати как и файл имплементации CMyShop.
Вот объявление класса:




//
//  IAPWrapper.h
//  Truck Adventure
//
//
//

#ifndef __Truck_Adventure__IAPWrapper__
#define __Truck_Adventure__IAPWrapper__

#import "IAPHelper_objc.h"
#import "IAPCallback.h"

class IAPWrapper
{
public:
    // А можно ли покупать ? Некоторые юзеры выключают внутренние покупки.
    bool canMakePayments();
    void requestProducts(iOSBridge::Callbacks::IAPCallback* callback);
    void setNullCallback();
    void buyProductIdentifier(const std::string& productID);
    int getProductIndexByIdentifier(const std::string& productID);
};

#endif /* defined(__Truck_Adventure__IAPWrapper__) */

Что здесь скажешь... смотрите реализацию
//
//  IAPWrapper.mm
//  Truck Adventure
//
//
//

#include "IAPWrapper.h"

bool IAPWrapper::canMakePayments()
{
    return [[IAPHelper_objc sharedHelper] canMakePayments];
}

void IAPWrapper::requestProducts(iOSBridge::Callbacks::IAPCallback* callback)
{
    [[IAPHelper_objc sharedHelper] requestProductsWithCallback: callback];
}

void IAPWrapper::setNullCallback()
{
    [[IAPHelper_objc sharedHelper] setNullCallback ];
}


void IAPWrapper::buyProductIdentifier(const std::string& productID)
{
    NSString *nsID = [NSString stringWithCString:productID.c_str()
                                encoding:[NSString defaultCStringEncoding]];
    
    [[IAPHelper_objc sharedHelper] buyProductIdentifier:nsID];
}

int IAPWrapper::getProductIndexByIdentifier(const std::string& productID)
{
    NSString *nsID = [NSString stringWithCString:productID.c_str()
                                        encoding:[NSString defaultCStringEncoding]];
    int index = [[IAPHelper_objc sharedHelper] getProductIndexByIdentifier:nsID];
    return index;
}

Обратите внимание на метод requestProducts(iOSBridge::Callbacks::IAPCallback* callback). Сюда передаем ссыклу на наш класс- магазин, потомок IAPCallback. Теперь посмотрим класс CMyShop, я оставил только то, что относится к статье. В любом случае, вы сами будете разрабатывать свой магазин внутри игры. Объявление класса:
//
//  Shop.h
//  Truck Adventure
//
//
//

#ifndef __Truck_Adventure__Shop__
#define __Truck_Adventure__Shop__

#include "cocos2d.h"
#include "Const.h"
#include "CarGarage.h"
#include "IntroLayer.h"
#include "Utils.h"
#include "IAPCallback.h"
#include "IAPWrapper.h"


using namespace cocos2d;

class TAShop : public CCLayer, public iOSBridge::Callbacks::IAPCallback
{
public:
    TAShop();
    ~TAShop();
    static cocos2d::CCScene* scene();
private:
    ...

    void onEnter()
    ...
    CCArray* mProducts;
    CCArray* mStoreKitProducts;

    // IN APP PURCHASE
    void requestServer();
    void productsDownloaded(CCArray* products);
    void productDonwloadFailed();
    // Callback когда транзакция успешна
    void productPurchased(std::string productID);
    // Callback когда транзакция восстановлена
    void productRestored(std::string productID);
    // Callback когда транзакция неуспешна
    void productCanceled(std::string productID);
    // здесь покупка
    void dealMe(std::string productID);
    ...
};

#endif /* defined(__Truck_Adventure__Shop__) */

Вот реализация:
...
void TAShop::onEnter()
{
    ...
    // запрос продуктов
    requestServer(); // можно вызвать и в конструкторе
    CCLayer::onEnter();
}

void TAShop::requestServer()
{
    // запрос продуктов
    IAPWrapper* IAP = new IAPWrapper::IAPWrapper();
    IAP->requestProducts(this); //передаем указатель на себя для Callback вызовов своих методов.
    delete IAP;
}

void TAShop::productsDownloaded(CCArray* products)
{

   // ответ от сервера со списком продуктов получен
   ...
}

void TAShop::productDonwloadFailed()
{
    // нет ответа от сервера
    ...
}


// Callback когда транзакция успешна
void TAShop::productPurchased(std::string productID)
{
    CCLog("SUCCESS TRANSACTION: Product = %s", productID.c_str());
}
// Callback когда транзакция восстановлена
void TAShop::productRestored(std::string productID)
{

    CCLog("RESTORE TRANSACTION: Product = %s", productID.c_str());
}
// Callback когда транзакция неуспешна
void TAShop::productCanceled(std::string productID)
{

    CCLog("CANCELED TRANSACTION: Product = %s", productID.c_str());
}
// купить продукт
void TAShop::dealMe(std::string productID)
{
   ...
   IAPWrapper* IAP = new IAPWrapper();
   iOSBridge::Callbacks::IAPItem* item = 
        (iOSBridge::Callbacks::IAPItem*)mStoreKitProducts->objectAtIndex(productIndex); // индекс продукта в массиве
   IAP->buyProductIdentifier(item->identification);
   delete IAP;
}
Ну вот и все. Надеюсь, что эта статья поможет вам реализовать IAP.
Вот архив:
http://yadi.sk/d/2DRID8Ej4gwWi