Auto-Renewable Subscription в 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];
 }
}


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

На этом все, мне это, в итоге, не пригодилось в данном проекте, но, надеюсь, что кому-то из вас повезет больше!
  • +7
  • 21,6k
  • 9
Поделиться публикацией

Похожие публикации

Комментарии 9
    0
    Жалко кармы у меня нет, так бы кикул вам жирный плюс. Спасибо за то, что делитесь опытом и предупреждаете о граблях
      0
      Описанное в статье ничем не отличается от того, что написано в официальной документации. Ну конечно не считая странной политики модерации таких подписок.
        0
        По поводу sandbox в доках написано следующее

        When testing auto-renewable in-app purchase subscriptions in the sandbox environment, the duration times will be compressed to allow for more streamlined testing. Additionally, a sandbox subscription will only auto-renew a maximum of 6 times

        Ну и приведена табличка соответствия длительности подписок для тестов.
          0
          Информация об этом написана только в огромном iTunes Connect Developer Guide, где-то на 200-ой странице.
          В документации к In-App, на чтении которой останавливается большинство, в том числе и я на первый момент, этой информации нет. Я просто потратил всю ночь, пока не понял, в чем причина такого поведения)
          In-App Purchase Programming Guide
            0
            Ну это вполне логично, что такая информация находится в доке про iTC, ссылка на него есть в In-App Purchase Programming Guide. Лень почитать нужный раздел — это не оправдание))
              0
              Просто когда читаешь In-App Purchase Programming Guide, может запросто сложиться ощущение его цельности и завершенности, так как информации для написания всего механизма там достаточно.
              И когда у тебя что-то работает не так, как надо, может и не возникнуть мысль о том, что беда сокрыта за пределами твоей реализации. Отсюда и бесснонная ночь)
          0
          Объясните принцип Non-renewing subscriptions? Получается что подписка действует для конкретного устройства, пока не заставишь пользователя привязаться к своему серверу после покупки?

          Еще не понятно, сейчас создал такой тип покупки, купил, потом жму еще раз купить мне выводиться сообщение что я уже купил эту подписку и предлагают «Нажмите купить чтобы возобновить или продлить ее», в этот момент с пользователя снова снимутся деньги? Как-то замудрено всё… :(
            0
            По первому вопросу—да, всё верно.
            При повторной же покупке никаких денег снимать не будут ни с кого, транзакция при этом обычная, не restorable.
              0
              Вы уверены? Тогда как мне снимать деньги с пользователя если подписка например на месяц и она у него закончилась?

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

          Самое читаемое