Auto-Renewable Subscription в iOS: правильная реализация и подводные камни

в 15:37, , рубрики: Cocoa, in-app purchases, iOS, OS X, Песочница, разработка под iOS, метки: , ,

Auto-Renewable Subscription, наверное, самый сложный из всех типов In-App Purchase в iOS, и реализовать его правильно, от начала и до конца, совсем непросто, и даже пройдя этот нелегкий путь, вы можете столкнуться с отказом цензоров принимать ваше приложение.

В данном посте я постараюсь провести вас через все этапы внедрения подписки и, возможно, смогу отговорить вас от этой идеи.

Что вообще такое Auto-Renewable Subscription

Данный вид In-App позволяет вам предложить пользователю подписаться на предоставляемый вами контент одним действием, и потом забыть о каких либо платежах — деньги с него будут списываться ежемесячно(срок подписки вы можете выбрать произвольно), и ему больше не нужно будет задумываться о том, что период подписки подходит к концу, а вам не придется лишний раз напоминать ему об этом — деньги будут сниматься с его счета и отправляться на ваш.

Также, вам не придется встраивать возможность отказаться от подписки — это возможно только в настройках iOS — удобно, и не пользователю не мозолит глаза кнопка отказа.

Почему мне могут отказать?

Казалось бы, такая идеальная схема, сделаю-ка я месяц триала, а потом пусть подписываются на доступ к приложению за доллар в месяц. Но не так-то все просто. Ключевыми словами здесь является то, что Auto-Renewable Subscription предназначены для предоставления цифрового контента (Digital Content), а это слова весьма размытые.

Например, наше приложение ежедневно предоставляет пользователю несколько новых слов, и мы думали, что это вполне себе «digital content». Парни из Apple так не думали. В итоге, пришлось переделывать все на Non-renewing subscriptions.

Поэтому, прежде чем начать внедрять этот тип покупок, подумайте, на самом ли деле вы подходите под то, что Apple вкладывала в смысл этого механизма.

Что ж, тогда перейдем к реализации

Процесс внедрения Auto-Renewable Subscription состоит из трех этапов:

  • Добавление и настройка в iTunes Connect
  • Настройка серверной части для валидации
  • Реализация покупки внутри приложения

Добавление и настройка в iTunes Connect

Сначала нам нужно получить shared secret, его мы будем использовать на серверной стороне при обращении к серверам Apple. Заходим в раздел Manage In-App Purchases и генерируем его.

Там же мы добавляем новую In-App соответсвующего типа, выбираем продолжительность, название и заполняем все поля.

Особенно нас интересует Product ID — его мы будем запрашивать из приложения.

Также нас попросят добавить ссылку на нашу Privcay Policy, без нее это невозможно. Что там писать? Да не так уж и важно, мы написали что-то такое: www.easy10.com/privacy/

Что ж, shared secret получили, Product ID есть, переходим к сервреной части.

Настройка серверной части для валидации

Этот этап необходим нам для проверки и дешифровки receipt, приходящих от Apple при покупке и в момент, когда нам интересно, обновлилась ли подписка в новом месяце.

Этот механизм реализован следущим образом:

  1. В приложении мы делаем запрос на покупку
  2. Получаем от Apple receipt
  3. Кодируем его в base64
  4. Отправляем на наш сервер (эти шаги можно поменять местами)
  5. С нашего сервера отправляем на сервера Apple
  6. Те его дешифруют и присылают нам
  7. Мы извлекаем необходимую информацию и говорим приложению, каков статус подписки

При отсутсвии собственного сервера, шаги 4-6 можно сделать с помощью сервиса www.beeblex.com

Если же вы хотите иметь полный контроль над процессом, то на шаге 5 вы должны отправить на buy.itunes.apple.com/verifyReceipt JSON следущего содержания

{
    "receipt-data" : "(receipt bytes here)",
    "password"     : "(shared secret bytes here)"//тот самый shared secret
}

В ответ на шаге 6 мы получим следущий JSON

{
    "status" : 0,
    "receipt" : { (receipt here) },
    "latest_receipt" : "(base-64 encoded receipt here)",
    "latest_receipt_info" : { (latest receipt info here) }
}

Если status = 0, то все хорошо. Если он равен 21006, значит наш пользователь не продлил подписку и нам нужно прекраить предоставление контента. Все остальные коды можно посмотреть здесь:
Status codes for auto-renewable subscriptions

В самом же поле receipt нас будет больше всего интересовать значение expires_date — сравниваем его с текущим и на основе этого принимаем какие-то решения. Если пользователь не отказался от вашей подписки, оно будет автоматически обновляться.

А как все это тестить?

Для этого нам необходимо создать тестового пользователя в iTunes Connect и на сервере отправлять запрос на проверку receipt на sandbox.itunes.apple.com/verifyReceipt

Хочу обратить ваше внимание на то, что в тестовом режиме подписка будет автоматически обновляться только пять раз. И все. Об этом почти нигде не написано, и я провел бессонные ночи, не понимая, в чем же причина того, что expires_date у меня не изменялась. Именно в этом. Поэтому вам придется создавать нового пользователя, чтобы тестить именно этом момент самопродления.

Помимо этого, период действия в тестовом режиме значительно короче чем он есть на самом деле, например, месячная подписка сжимается до 5 минут.

Ну и на последок я приведу код покупок в самом приложении. Хоть он и был написан в сотнях мест, раз уж я решил собрать все вместе, без этой части не обойтись.

Реализация покупки внутри приложения

Для этого в iOS предназначен StoreKit.framework. Он содержит в себе все необходмое для совершения покупки, отслеживания прохождения транзакции и восстановления покупок с нового устройства.

Для начала нам нужно будет добавить обзервера транзаций, который должен пооддерживать протоколы SKPaymentTransactionObserver — это нужно для того, чтобы транзакция не потерялась, когда у пользователя внезапно выключился телефон или интернет.Поэтому это лучше всего делать где-нибудь в

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
////
[[SKPaymentQueue defaultQueue] addTransactionObserver:sharedPaymentsOberver];//это будет синглтон
///}

Создадим класс PaymentsOberver, который будет пооддеживать все необходимые нам протоколы


#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

@interface PaymentsObserver : NSObject <SKPaymentTransactionObserver,SKProductsRequestDelegate,SKRequestDelegate>
- (void)requestProductData:(NSString*)ofProduct;
- (BOOL)validateReceipt;
@end

Сначала мы должны сформировать запрос на продукт, указв Product ID, который нам необходим, мы можем запросить неограниченное количество продуктов сразу


- (void)requestProductData:(NSString*)ofProduct
{
    if ([SKPaymentQueue canMakePayments])//Проверяем, включена ли возможность совершать покупки
    {
        NSLog(@"Request Began");
        SKProductsRequest *request= [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithObject:ofProduct]];
        request.delegate = self;
        [request start];
    } else { 
      UIAlertView *noPayment = [[UIAlertView alloc]initWithTitle:NSLocalizedString(@"You can't make payments", nil   )  message:NSLocalizedString(@"Enable payments in your account settings,please", nil)  delegate:self cancelButtonTitle:NSLocalizedString(@"Ok", nil) otherButtonTitles: nil];
            [noPayment show];
    }   
}

По завершении запроса мы получим вызов одного из методов SKProductRequestDelegate, в случае успеха — вот такой


- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    NSArray *myProduct = response.products;
    if ([response.products count]) {
        SKPayment *payment = [SKPayment paymentWithProduct:[myProduct lastObject]];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
}

В нем мы добавляем полученый продукт в очередь транзакций. Дальше с ним разбирается Apple, выкидывая пользователю окно с вопросом о том, уверен ли он в покупке. По окончанию этого процесса мы имеем вызов SKPaymentTransactionObserver, в котором, в зависимости от того, прошла ли транзакция, мы продолжаем наш процесс, передавая receipt серверу на проверку.


- (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;
        }
    }
}

В случае успешной проверки нашего receipt, мы должны предоставить пользователю контент и, что очень важно, удалить транзакцию из очереди.


- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
   if ([self validateReceipt:transaction.transactionReceipt]) {
    [self provideContent: transaction.payment.productIdentifier];
    // Remove the transaction from the payment queue.
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
 }
}

При завершении периода подписки нужно будет просто отправлять запрос к серверу и проверять, продлена ли она или нет.

На этом все, мне это, в итоге, не пригодилось в данном проекте, но, надеюсь, что кому-то из вас повезет больше!

Автор: pestrov

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js