Как стать автором
Обновить
0
Apphud
Сервис для работы с подписками в iOS-приложениях

Как проверить доступность вводного предложения в iOS

Время на прочтение5 мин
Количество просмотров2.7K

Если в вашем приложении с подписками вы используете вводные предложения (триал, оплата по мере использования или предоплата), то прежде чем показать цену на экране оплаты, вам нужно определить доступность вводного предложения для пользователя. Если пользователь до этого уже оформлял триал, то для него вы должны отображать полную цену.


image


Всем привет, на связи Ренат из Apphud – сервиса, который упрощает работу с подписками в iOS-приложениях. Сегодня я расскажу, как определить, есть ли у отдельно взятого пользователя право активировать вводное предложение или нет.


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


В документации Apple есть схема, показывающая, в каких случаях вводное предложение доступно для пользователя:



Получается, пользователь может воспользоваться вводным предложением, если:


  • ранее он не использовал вводное предложение

И


  • подписка либо еще не была оформлена, либо уже истекла

Для проверки доступности вводного предложения нужно выполнить 3 шага:


1) Провалидировать App Store-чек и вытащить массив транзакций. Если транзакций, нет, то ничего не проверяем, вводное предложение доступно. Если транзакции есть, то выполнить следующие два шага.


2) Проверить, было ли ранее использовано вводного предложение


3) Проверить текущий статус подписки


Рассмотрим подробнее эти шаги.


1. Валидация App Store чека


Для валидации чека нужно отправить запрос в Apple, передав receiptData и sharedSecret. Замените значение sharedSecret на ваше собственное. Если не знаете ваш sharedSecret, то здесь описано, где его взять.


func isEligibleForIntroductory(callback: @escaping (Bool) -> Void){

        guard let receiptUrl = Bundle.main.appStoreReceiptURL else {
            callback(true)
            return
        }

        #if DEBUG
            let urlString = "https://sandbox.itunes.apple.com/verifyReceipt"
        #else 
            let urlString = "https://buy.itunes.apple.com/verifyReceipt"
        #endif

        let receiptData = try? Data(contentsOf: receiptUrl).base64EncodedString()

        let sharedSecret = "YOUR_SHARED_SECRET"

        let requestData = ["receipt-data" : receiptData ?? "", "password" : sharedSecret, "exclude-old-transactions" : false] as [String : Any]
        var request = URLRequest(url: URL(string: urlString)!)
        request.httpMethod = "POST"
        request.setValue("Application/json", forHTTPHeaderField: "Content-Type")
        let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: [])
        request.httpBody = httpBody
        URLSession.shared.dataTask(with: request)  { (data, response, error) in
            // continue here
        }.resume()   
    }

В примере выше используется макрос #if DEBUG для определения типа подписки: sandbox или production. Если вы используете другие макросы, то вам нужно будет обновить код в этом месте.

2. Проверка, использовалось ли вводное предложение ранее


Получив ответ от Apple, переводим его в Dictionary и получаем массив транзакций:


// paste this code after "continue here" comment
guard let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String : AnyHashable], let receipts_array = json["latest_receipt_info"] as? [[String : AnyHashable]] else {
      callback(true)
      return
}

// continue here

Проходимся по массиву транзакций и смотрим значения is_trial_period и is_in_intro_offer_period. Если одно из значений true, то пользователь уже оформлял вводное предложение. Эти значения приходят как строка, потому для надежности попытаемся конвертировать значение и в Bool, и в строку.


// paste this code after "continue here" comment
var latestExpiresDate = Date(timeIntervalSince1970: 0)
let formatter = DateFormatter()
for receipt in receipts_array {
  let used_trial : Bool = receipt["is_trial_period"] as? Bool ?? false || (receipt["is_trial_period"] as? NSString)?.boolValue ?? false
  let used_intro : Bool = receipt["is_in_intro_offer_period"] as? Bool ?? false || (receipt["is_in_intro_offer_period"] as? NSString)?.boolValue ?? false
  if used_trial || used_intro {
    callback(false)
    return
  }
// continue here

3. Проверка текущего статуса подписки


Чтобы узнать текущий статус подписки, мы должны найти самый поздний expires_date и сравнить с текущей датой. Если подписка еще не истекла, то вводное предложение недоступно:


// paste this code after "continue here" comment
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
if let expiresDateString = receipt["expires_date"] as? String, let date = formatter.date(from: expiresDateString) {
    if date > latestExpiresDate {
      latestExpiresDate = date
    }
  }
}

if latestExpiresDate > Date() {
  callback(false)
} else {
  callback(true)
}

Ссылку на полный код метода вы сможете найти в конце статьи, однако в данном методе есть много "Но".


Подводные камни


  • В этом примере мы рассмотрели лишь случай с одной группой подписок. Если вы используете более одной группы подписок в приложении, то вы должны передавать в этот метод идентификатор группы подписок и сверять его по значению subscription_group_identifier в receipt.


  • В этом примере не учтен случай возвратов подписок. Для этого необходимо проверять наличие поля cancellation_date:


    if receipt["cancellation_date"] != nil{
    // if user made a refund, no need to check for eligibility
    callback(false)
    return
    }

  • А еще здесь не учитывается льготный период (Billing Grace Period). Если пользователь на момент валидации чека находится в льготном периоде, то в pending_renewal_info будет поле grace_period_expires_date. В этом случае вы как разработчик обязаны предоставить премиум функционал пользователю без отображения экрана оплаты. И соответственно, нет смысла проверять доступность вводного предложения.


  • Существует проблема, связанная с проверкой даты истечения. Системное время на iOS устройстве можно открутить и тогда наш код будет выдавать неверный результат:  подписка будет считаться активной.


  • Валидация чека на самом устройстве не рекомендуется Apple. Они несколько раз говорили об этом на WWDC (с 5:50) и это указано в документации. Это небезопасно, потому что злоумышленник может перехватить данные с помощью man-in-the-middle атаки. Apple рекомендует использовать свой сервер для валидации чеков.



Проверка доступности промо-предложения


Условие доступности промо-предложения проще – главное, чтобы у пользователя была активная или истекшая подписка. Для этого нужно смотреть наличие pending_renewal_info для вашей группы подписок.


Как это реализовано в Apphud SDK


Достаточно вызвать один метод, передав в него ваш product, который вернет вам результат:


Apphud.checkEligibilityForIntroductoryOffer(product: myProduct) { result in
  if result {
    // User is eligible to purchase introductory offer
  }
}

И аналогично для промо-преложения:


Apphud.checkEligibilityForPromotionalOffer(product: myProduct) { result in
  if result {
    // User is eligible to purchase promotional offer
  }
}

Также есть методы проверки доступности сразу для нескольких продуктов за один вызов:


func checkEligibilitiesForIntroductoryOffers(products: [SKProduct], callback: ApphudEligibilityCallback)

func checkEligibilitiesForPromotionalOffers(products: [SKProduct], callback: ApphudEligibilityCallback)

Заключение


Полный код метода можете скачать здесь.


Мы в Apphud уже реализовали проверку доступности вводного и промо-предложений в удобном open-source SDK. А еще Apphud помогает отслеживать статус подписки, анализировать ключевые метрики, автоматически предлагать скидки отписавшимся пользователям и многое другое. Если при работе с подписками вы испытываете боль, попробуйте наше решение бесплатно.
Теги:
Хабы:
+7
Комментарии4

Публикации

Изменить настройки темы

Информация

Сайт
apphud.com
Дата регистрации
Дата основания
Численность
2–10 человек
Местоположение
Россия
Представитель
Денис Миннетдинов

Истории