Если в вашем приложении с подписками вы используете вводные предложения (триал, оплата по мере использования или предоплата), то прежде чем показать цену на экране оплаты, вам нужно определить доступность вводного предложения для пользователя. Если пользователь до этого уже оформлял триал, то для него вы должны отображать полную цену.
Всем привет, на связи Ренат из 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 помогает отслеживать статус подписки, анализировать ключевые метрики, автоматически предлагать скидки отписавшимся пользователям и многое другое. Если при работе с подписками вы испытываете боль, попробуйте наше решение бесплатно.