Всем привет!

Если вы читаете данный пост то, наверняка вы прочитали первую его часть — In-App Purchase: Полное руководство #1.

Итак продолжим. Мы создали App ID, добавили приложение в iTunes Connect, добавили In-App Purchase Product, сделали все необходимые настройки, написали код для извлечения product description и, — успешно извлекли его! Мы выбрались!

good_year2

Теперь осталось дело за малым.

8. Пишем код для осуществления покупки In-App Purchase

Итак. Первое, что вы должны понимать это то, что вы ответственны за UI организации процесса покупки. StoreKit не содержит никаких UI элементов. Но, если вы внимательно читали первую часть, то наверняка обратили внимание на header InAppPurchaseManager.h

#define kInAppPurchaseManagerProductsFetchedNotification @"kInAppPurchaseManagerProductsFetchedNotification"
#define kInAppPurchaseManagerTransactionFailedNotification @"kInAppPurchaseManagerTransactionFailedNotification"
#define kInAppPurchaseManagerTransactionSucceededNotification @"kInAppPurchaseManagerTransactionSucceededNotification"
#define kInAppPurchaseManagerTransactionCanceledNotification @"kInAppPurchaseManagerTransactionCanceledNotification"

Это нотификации, которые мы будем посылать в InAppPurchaseManager в случае успеха или провала, и на которые нужно подписать ваш UIViewController:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(productsFetched:)
                                                 name:kInAppPurchaseManagerProductsFetchedNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(transactionSucceeded:)
                                                 name:kInAppPurchaseManagerTransactionSucceededNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(transactionFailed:)
                                                 name:kInAppPurchaseManagerTransactionFailedNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(transactionCanceled:)
                                                 name:kInAppPurchaseManagerTransactionCanceledNotification
                                               object:nil];
}

Весь код далее это backend для процесса осуществления транзакции. Это простой класс с простым API, который вне UI и может вызываться для осуществления покупки.

Наш менеджер покупок должен отвечать протоколам

@interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate, SKPaymentTransactionObserver>

И теперь, собственно, сам код.

// InAppPurchaseManager.m
#define kInAppPurchaseProUpgradeProductId @"com.myCompany.myAwesomeApp.myProductID"

…

#pragma mark - Public methods

/*
 * call this method once on startup
 */
- (void)loadStore
{
    // restarts any purchases if they were interrupted last time the app was open
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    
    // get the product description (defined in early sections)
    [self requestProUpgradeProductData];
}

/*
 * call this before making a purchase
 */
- (BOOL)canMakePurchases
{
    return [SKPaymentQueue canMakePayments];
}

/*
 * kick off the upgrade transaction
 */
- (void)purchaseProUpgrade
{
    SKPayment *payment = [SKPayment paymentWithProductIdentifier:kInAppPurchaseProUpgradeProductId];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

#pragma -
#pragma Purchase helpers

/*
 * saves a record of the transaction by storing the receipt to disk
 */
- (void)recordTransaction:(SKPaymentTransaction *)transaction
{
    if ([transaction.payment.productIdentifier isEqualToString:kInAppPurchaseProUpgradeProductId])
    {
        // save the transaction receipt to disk
        [[NSUserDefaults standardUserDefaults] setValue:transaction.transactionReceipt forKey:@"proUpgradeTransactionReceipt" ];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}

/*
 * enable pro features
 */
- (void)provideContent:(NSString *)productId
{
    if ([productId isEqualToString:kInAppPurchaseProUpgradeProductId])
    {
        // enable the pro features
        [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isProUpgradePurchased" ];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}

/*
 * removes the transaction from the queue and posts a notification with the transaction result
 */
- (void)finishTransaction:(SKPaymentTransaction *)transaction wasSuccessful:(BOOL)wasSuccessful
{
    // remove the transaction from the payment queue.
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    
    NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:transaction, @"transaction" , nil];
    if (wasSuccessful)
    {
        // send out a notification that we’ve finished the transaction
        [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionSucceededNotification object:self userInfo:userInfo];
    }
    else
    {
        // send out a notification for the failed transaction
        [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionFailedNotification object:self userInfo:userInfo];
    }
}

/*
 * called when the transaction was successful
 */
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    [self recordTransaction:transaction];
    [self provideContent:transaction.payment.productIdentifier];
    [self finishTransaction:transaction wasSuccessful:YES];
}

/*
 * called when a transaction has been restored and and successfully completed
 */
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
    [self recordTransaction:transaction.originalTransaction];
    [self provideContent:transaction.originalTransaction.payment.productIdentifier];
    [self finishTransaction:transaction wasSuccessful:YES];
}

/*
 * called when a transaction has failed
 */
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
    if (transaction.error.code != SKErrorPaymentCancelled)
    {
        // error!
        [self finishTransaction:transaction wasSuccessful:NO];
    }
    else
    {
        // this is fine, the user just cancelled, so don’t notify
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }
}

#pragma mark -
#pragma mark SKPaymentTransactionObserver methods

/*
 * called when the transaction status is updated
 */
- (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];
                break;
            default:
                break;
        }
    }
}

Несколько слов о том, как это работает в целом.

Когда мы загружаем вьюху с нашим продуктом, мы вызываем публичный метод loadStore, который в свою очередь вызывает requestProUpgradeProductData, который возвращает в метод делегата productsRequest:didReceiveResponse: наш response с productID — это то, что мы делали в первой части.

После этого мы нажимаем кнопку «Купить». По нажатию на эту кнопку мы проверяем, можем ли мы совершать покупку и есть ли у нас продукт, который мы хотим купить. Если да, то вызываем purchaseProUpgrade

    if([[InAppPurchaseManager sharedManager] canMakePurchases] && proUpgradeProduct)
    {
        [[InAppPurchaseManager sharedManager] purchaseProUpgrade];
    }

Последний создает SKPayment и добавляет его в SKPaymentQueue. Далее при каждом изменении транзакции вызовется метод делегата  SKPaymentTransactionObserver — paymentQueue:updatedTransactions: который финализирует процесс покупки.

В данном примере использована очень простая логика сохранения данных о том, что фича куплена

        [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isProUpgradePurchased" ];
        [[NSUserDefaults standardUserDefaults] synchronize];

В виду того, что данный подход очень легко взломать, рекомендую использовать более изощренные методы сохранения признака приобретения фич в ваших проектах.

Еще есть такое мнение, что Apple предпочитает, чтобы вы давали пользователю возможность восстанавливать свои покупки, если он, к примеру, удалил приложение, и после установил заново. Мол, кнопка Restore на равне с кнопкой Buy не помешает. Кто-то говорил, что без кнопки Restore даже могут завернуть приложение и не принять в AppStore, хотя я видел массу приложений без нее. 

На этом все. В третьей, заключительной части поговорим о том, как загрузить контент с серверов Apple и как протестировать покупки.

Удачи!

Продолжение следует…

In-App Purchase: Полное руководство — #3